test: fix most e2e tests
This commit is contained in:
@@ -137,6 +137,39 @@ describe('PluginClientApiService', () => {
|
||||
expect(context.messageRevisions.broadcastRevision).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('applies an edit issued immediately after sending a message', async () => {
|
||||
const api = context.service.createApi(TEST_MANIFEST);
|
||||
const sent = api.messages.send('Plugin API original message');
|
||||
|
||||
api.messages.edit(sent.id, 'Plugin API edited message');
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
const editDispatched = context.store.dispatch.mock.calls.some(
|
||||
([action]) => action.type === MessagesActions.editMessageSuccess.type
|
||||
&& action.messageId === sent.id
|
||||
&& action.content === 'Plugin API edited message'
|
||||
);
|
||||
|
||||
expect(editDispatched).toBe(true);
|
||||
});
|
||||
|
||||
it('applies a delete issued immediately after sending a message', async () => {
|
||||
const api = context.service.createApi(TEST_MANIFEST);
|
||||
const sent = api.messages.send('Plugin API deleted message');
|
||||
|
||||
api.messages.delete(sent.id);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
const deleteDispatched = context.store.dispatch.mock.calls.some(
|
||||
([action]) => action.type === MessagesActions.deleteMessageSuccess.type
|
||||
&& action.messageId === sent.id
|
||||
);
|
||||
|
||||
expect(deleteDispatched).toBe(true);
|
||||
});
|
||||
|
||||
it('publishes typing state through the realtime facade', () => {
|
||||
const api = context.service.createApi(TEST_MANIFEST);
|
||||
|
||||
@@ -287,6 +320,12 @@ interface ServiceTestContext {
|
||||
createSignedRevision: ReturnType<typeof vi.fn>;
|
||||
persistRevision: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
db: {
|
||||
getMessageById: ReturnType<typeof vi.fn>;
|
||||
saveMessage: ReturnType<typeof vi.fn>;
|
||||
updateMessage: ReturnType<typeof vi.fn>;
|
||||
updateRoom: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
}
|
||||
|
||||
function createServiceTestContext(): ServiceTestContext {
|
||||
@@ -354,6 +393,15 @@ function createServiceTestContext(): ServiceTestContext {
|
||||
onSignalingMessage: new Subject<unknown>(),
|
||||
sendRawMessage: vi.fn()
|
||||
};
|
||||
const messageDb = new Map<string, Message>();
|
||||
const db = {
|
||||
getMessageById: vi.fn(async (id: string) => messageDb.get(id) ?? null),
|
||||
saveMessage: vi.fn(async (message: Message) => {
|
||||
messageDb.set(message.id, message);
|
||||
}),
|
||||
updateMessage: vi.fn(async () => undefined),
|
||||
updateRoom: vi.fn(async () => undefined)
|
||||
};
|
||||
const store = {
|
||||
dispatch: vi.fn(),
|
||||
selectSignal: vi.fn((selector: unknown) => {
|
||||
@@ -401,12 +449,7 @@ function createServiceTestContext(): ServiceTestContext {
|
||||
},
|
||||
{
|
||||
provide: DatabaseService,
|
||||
useValue: {
|
||||
getMessageById: vi.fn(async () => null),
|
||||
saveMessage: vi.fn(async () => undefined),
|
||||
updateMessage: vi.fn(async () => undefined),
|
||||
updateRoom: vi.fn(async () => undefined)
|
||||
}
|
||||
useValue: db
|
||||
},
|
||||
{
|
||||
provide: MessageRevisionService,
|
||||
@@ -485,7 +528,8 @@ function createServiceTestContext(): ServiceTestContext {
|
||||
store,
|
||||
uiRegistry,
|
||||
voice,
|
||||
messageRevisions
|
||||
messageRevisions,
|
||||
db
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -68,6 +68,15 @@ export class PluginClientApiService {
|
||||
private readonly voice = inject(VoiceConnectionFacade);
|
||||
private readonly messageRevisions = inject(MessageRevisionService);
|
||||
|
||||
/**
|
||||
* Serializes plugin-initiated message create/edit/delete operations. `send`
|
||||
* returns its draft synchronously while the create persists asynchronously, so
|
||||
* an edit/delete issued on the returned id must wait for that create to land in
|
||||
* the database - otherwise its `getMessageById` lookup races ahead of the
|
||||
* pending `saveMessage` and the mutation is silently dropped.
|
||||
*/
|
||||
private messageMutationChain: Promise<void> = Promise.resolve();
|
||||
|
||||
private readonly currentMessages = this.store.selectSignal(selectCurrentRoomMessages);
|
||||
private readonly currentRoom = this.store.selectSignal(selectCurrentRoom);
|
||||
private readonly currentRoomChannels = this.store.selectSignal(selectCurrentRoomChannels);
|
||||
@@ -630,7 +639,7 @@ export class PluginClientApiService {
|
||||
}
|
||||
|
||||
private deletePluginMessage(pluginId: string, messageId: string): void {
|
||||
void this.emitPluginMessageMutation(pluginId, messageId, {
|
||||
this.enqueueMessageMutation(() => this.emitPluginMessageMutation(pluginId, messageId, {
|
||||
type: 'plugin-delete',
|
||||
apply: async (existing, editedAt) => this.messageRevisions.createSignedRevision({
|
||||
message: existing,
|
||||
@@ -647,11 +656,11 @@ export class PluginClientApiService {
|
||||
type: 'message-deleted'
|
||||
}),
|
||||
dispatch: () => MessagesActions.deleteMessageSuccess({ messageId })
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
private editPluginMessage(pluginId: string, messageId: string, content: string): void {
|
||||
void this.emitPluginMessageMutation(pluginId, messageId, {
|
||||
this.enqueueMessageMutation(() => this.emitPluginMessageMutation(pluginId, messageId, {
|
||||
type: 'plugin-edit',
|
||||
content,
|
||||
apply: async (existing, editedAt) => this.messageRevisions.createSignedRevision({
|
||||
@@ -674,7 +683,7 @@ export class PluginClientApiService {
|
||||
editedAt: message.editedAt ?? Date.now(),
|
||||
messageId
|
||||
})
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
private sendPluginMessage(pluginId: string, content: string, channelId?: string): Message {
|
||||
@@ -694,7 +703,7 @@ export class PluginClientApiService {
|
||||
revision: 0
|
||||
};
|
||||
|
||||
void this.emitPluginMessageRevision(pluginId, {
|
||||
this.enqueueMessageMutation(() => this.emitPluginMessageRevision(pluginId, {
|
||||
draftMessage,
|
||||
type: 'create',
|
||||
actorId: currentUser?.id ?? pluginId,
|
||||
@@ -702,11 +711,22 @@ export class PluginClientApiService {
|
||||
pluginId,
|
||||
sign: !!currentUser,
|
||||
dispatch: (message) => MessagesActions.sendMessageSuccess({ message })
|
||||
});
|
||||
}));
|
||||
|
||||
return draftMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Chains a plugin message operation onto the serial mutation queue so create,
|
||||
* edit, and delete calls apply in the order the plugin issued them, regardless of
|
||||
* how their individual async persistence steps would otherwise interleave.
|
||||
*/
|
||||
private enqueueMessageMutation(task: () => Promise<void>): void {
|
||||
this.messageMutationChain = this.messageMutationChain
|
||||
.then(() => task())
|
||||
.catch(() => undefined);
|
||||
}
|
||||
|
||||
private async emitPluginMessageRevision(
|
||||
pluginId: string,
|
||||
input: {
|
||||
|
||||
Reference in New Issue
Block a user