mirror of
https://github.com/block/buzz.git
synced 2026-10-29 13:31:44 +00:00
test(mentions): prove editor cancellation with native range restoration
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Signed-off-by: Logan Johnson <loganj@squareup.com>
This commit is contained in:
@@ -239,9 +239,12 @@ function Composer() {
|
||||
{
|
||||
onKeyDown: (event) => {
|
||||
// Only editor events reach this bridge, as in MessageComposer.
|
||||
if (event.key === "Tab" && event.shiftKey &&
|
||||
event.target === richText.editor.view.dom &&
|
||||
focusMentionOptionsTrigger(formRef.current)) {
|
||||
if (
|
||||
event.key === "Tab" &&
|
||||
event.shiftKey &&
|
||||
event.target === richText.editor.view.dom &&
|
||||
focusMentionOptionsTrigger(formRef.current)
|
||||
) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
@@ -258,7 +261,9 @@ function Composer() {
|
||||
React.createElement(MentionAutocomplete, {
|
||||
composerOwnsFocus: true,
|
||||
keepMentionedAgentsPinned: keepPinned,
|
||||
onKeepMentionedAgentsPinnedChange: state.withOptions ? setKeepPinned : undefined,
|
||||
onKeepMentionedAgentsPinnedChange: state.withOptions
|
||||
? setKeepPinned
|
||||
: undefined,
|
||||
isOpen: mention.isMentionOpen,
|
||||
isLoading: mention.isMentionLoading,
|
||||
suggestions: mention.suggestions,
|
||||
@@ -495,19 +500,30 @@ for (const mode of ["Enter", "pin"]) {
|
||||
test(`navigation: editor ShiftTab abandons ${mode} even after return`, async () => {
|
||||
await setup({ owner: OTHER, visible: true, directoryVisible: true });
|
||||
let resolve;
|
||||
state.fresh = new Promise((r) => { resolve = r; });
|
||||
state.fresh = new Promise((r) => {
|
||||
resolve = r;
|
||||
});
|
||||
await act(async () => {
|
||||
richText.editor.view.dom.focus();
|
||||
choose(mode);
|
||||
});
|
||||
unchanged();
|
||||
await act(async () => {
|
||||
richText.editor.view.dom.dispatchEvent(new dom.window.KeyboardEvent("keydown", {
|
||||
key: "Tab", shiftKey: true, bubbles: true, cancelable: true,
|
||||
}));
|
||||
richText.editor.view.dom.dispatchEvent(
|
||||
new dom.window.KeyboardEvent("keydown", {
|
||||
key: "Tab",
|
||||
shiftKey: true,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
}),
|
||||
);
|
||||
const trigger = document.querySelector("[data-mention-options-trigger]");
|
||||
assert.ok(trigger, "chat Options trigger exists");
|
||||
assert.equal(document.activeElement === trigger, true, "ShiftTab focuses Options");
|
||||
assert.equal(
|
||||
document.activeElement === trigger,
|
||||
true,
|
||||
"ShiftTab focuses Options",
|
||||
);
|
||||
richText.editor.view.dom.focus();
|
||||
});
|
||||
await act(async () => resolve([rawAgent()]));
|
||||
@@ -521,65 +537,110 @@ for (const shiftKey of [false, true]) {
|
||||
test(`navigation: focused pin ${shiftKey ? "ShiftTab" : "Tab"} depart=${depart}`, async () => {
|
||||
await setup({ owner: OTHER, visible: true, directoryVisible: true });
|
||||
let resolve;
|
||||
state.fresh = new Promise((r) => { resolve = r; });
|
||||
const toggle = document.querySelector(`[data-testid="mention-always-address-${AGENT}"]`);
|
||||
state.fresh = new Promise((r) => {
|
||||
resolve = r;
|
||||
});
|
||||
const toggle = document.querySelector(
|
||||
`[data-testid="mention-always-address-${AGENT}"]`,
|
||||
);
|
||||
assert.ok(toggle, "production pin Toggle exists");
|
||||
await act(async () => {
|
||||
toggle.focus();
|
||||
assert.equal(document.activeElement === toggle, true, "keyboard pin owns focus");
|
||||
assert.equal(
|
||||
document.activeElement === toggle,
|
||||
true,
|
||||
"keyboard pin owns focus",
|
||||
);
|
||||
const key = shiftKey ? " " : "Enter";
|
||||
const event = new dom.window.KeyboardEvent("keydown", {
|
||||
key, bubbles: true, cancelable: true,
|
||||
key,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
toggle.dispatchEvent(event);
|
||||
assert.equal(event.defaultPrevented, false, "overlay activation stays native");
|
||||
assert.equal(state.freshCalls ?? 0, 0, "pin keydown must not select in editor");
|
||||
assert.equal(
|
||||
event.defaultPrevented,
|
||||
false,
|
||||
"overlay activation stays native",
|
||||
);
|
||||
assert.equal(
|
||||
state.freshCalls ?? 0,
|
||||
0,
|
||||
"pin keydown must not select in editor",
|
||||
);
|
||||
const release = new dom.window.KeyboardEvent("keyup", {
|
||||
key, bubbles: true, cancelable: true,
|
||||
key,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
// JSDOM lacks native activation: Enter clicks on keydown, Space on keyup.
|
||||
if (key === " ") toggle.dispatchEvent(release);
|
||||
if (!event.defaultPrevented && !release.defaultPrevented) toggle.click();
|
||||
if (!event.defaultPrevented && !release.defaultPrevented)
|
||||
toggle.click();
|
||||
});
|
||||
unchanged();
|
||||
assert.equal(state.freshCalls, 1);
|
||||
if (depart) await act(async () => {
|
||||
const event = new dom.window.KeyboardEvent("keydown", {
|
||||
key: "Tab", shiftKey, bubbles: true, cancelable: true,
|
||||
if (depart)
|
||||
await act(async () => {
|
||||
const event = new dom.window.KeyboardEvent("keydown", {
|
||||
key: "Tab",
|
||||
shiftKey,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
toggle.dispatchEvent(event);
|
||||
assert.equal(
|
||||
event.defaultPrevented,
|
||||
false,
|
||||
"overlay Tab stays native",
|
||||
);
|
||||
const outside = document.createElement("button");
|
||||
document.body.append(outside);
|
||||
if (!event.defaultPrevented) outside.focus();
|
||||
assert.equal(
|
||||
document.activeElement === outside,
|
||||
true,
|
||||
"Tab departs pin",
|
||||
);
|
||||
toggle.focus();
|
||||
});
|
||||
toggle.dispatchEvent(event);
|
||||
assert.equal(event.defaultPrevented, false, "overlay Tab stays native");
|
||||
const outside = document.createElement("button");
|
||||
document.body.append(outside);
|
||||
if (!event.defaultPrevented) outside.focus();
|
||||
assert.equal(document.activeElement === outside, true, "Tab departs pin");
|
||||
toggle.focus();
|
||||
});
|
||||
await act(async () => resolve([rawAgent()]));
|
||||
await settle();
|
||||
if (depart) {
|
||||
unchanged();
|
||||
assert.equal(picker.announcement, "");
|
||||
} else {
|
||||
assert.deepEqual(effects.filter(([kind]) => kind === "promote"), [["promote", AGENT]]);
|
||||
assert.deepEqual(effects.filter(([kind]) => kind === "autoPin"), []);
|
||||
assert.deepEqual(effects.filter(([kind]) => kind === "provenance"), [
|
||||
["provenance", [{ pubkey: AGENT, prefix: "@Remote Scout " }]],
|
||||
]);
|
||||
assert.deepEqual(
|
||||
effects.filter(([kind]) => kind === "promote"),
|
||||
[["promote", AGENT]],
|
||||
);
|
||||
assert.deepEqual(
|
||||
effects.filter(([kind]) => kind === "autoPin"),
|
||||
[],
|
||||
);
|
||||
assert.deepEqual(
|
||||
effects.filter(([kind]) => kind === "provenance"),
|
||||
[["provenance", [{ pubkey: AGENT, prefix: "@Remote Scout " }]]],
|
||||
);
|
||||
assert.equal(effects.filter(([kind]) => kind === "edit").length, 2);
|
||||
assert.deepEqual(getMentionSelectionHistory(VIEWER, CHANNEL), []);
|
||||
assert.equal(richText.getPlainTextAndCursor().text, "@Remote Scout ");
|
||||
assert.equal(mention.getDraftMentionRefs("@Remote Scout ")[0]?.pubkey, AGENT);
|
||||
assert.equal(
|
||||
mention.getDraftMentionRefs("@Remote Scout ")[0]?.pubkey,
|
||||
AGENT,
|
||||
);
|
||||
// MentionHighlightExtension decorates literal @labels from either path;
|
||||
// .mention-chip is not selection provenance. The pin witnesses above
|
||||
// distinguish admission paths; verify the prefix is plain document text
|
||||
// (useRichTextEditor.replacePlainTextRange), not an embedded mention node.
|
||||
assert.deepEqual(richText.editor.getJSON(), {
|
||||
type: "doc",
|
||||
content: [{
|
||||
type: "paragraph",
|
||||
content: [{ type: "text", text: "@Remote Scout " }],
|
||||
}],
|
||||
content: [
|
||||
{
|
||||
type: "paragraph",
|
||||
content: [{ type: "text", text: "@Remote Scout " }],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -588,10 +649,16 @@ for (const shiftKey of [false, true]) {
|
||||
|
||||
for (const departure of ["window", "no-Options native fallback"]) {
|
||||
test(`navigation: ${departure} abandons pending selection after return`, async () => {
|
||||
await setup({ owner: OTHER, visible: true, directoryVisible: true,
|
||||
withOptions: departure !== "no-Options native fallback" });
|
||||
await setup({
|
||||
owner: OTHER,
|
||||
visible: true,
|
||||
directoryVisible: true,
|
||||
withOptions: departure !== "no-Options native fallback",
|
||||
});
|
||||
let resolve;
|
||||
state.fresh = new Promise((r) => { resolve = r; });
|
||||
state.fresh = new Promise((r) => {
|
||||
resolve = r;
|
||||
});
|
||||
await act(async () => {
|
||||
richText.editor.view.dom.focus();
|
||||
choose("Enter");
|
||||
@@ -603,20 +670,38 @@ for (const departure of ["window", "no-Options native fallback"]) {
|
||||
if (departure === "window") {
|
||||
// Window departure can retain activeElement; dispatch only that boundary.
|
||||
dom.window.dispatchEvent(new dom.window.Event("blur"));
|
||||
assert.equal(document.activeElement === editor, true, "window blur retains editor identity");
|
||||
assert.equal(
|
||||
document.activeElement === editor,
|
||||
true,
|
||||
"window blur retains editor identity",
|
||||
);
|
||||
dom.window.dispatchEvent(new dom.window.Event("focus"));
|
||||
} else {
|
||||
assert.equal(document.querySelector("[data-mention-options-trigger]") === null, true);
|
||||
assert.equal(
|
||||
document.querySelector("[data-mention-options-trigger]") === null,
|
||||
true,
|
||||
);
|
||||
const event = new dom.window.KeyboardEvent("keydown", {
|
||||
key: "Tab", shiftKey: true, bubbles: true, cancelable: true,
|
||||
key: "Tab",
|
||||
shiftKey: true,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
editor.dispatchEvent(event);
|
||||
assert.equal(event.defaultPrevented, false, "no Options leaves ShiftTab native");
|
||||
assert.equal(
|
||||
event.defaultPrevented,
|
||||
false,
|
||||
"no Options leaves ShiftTab native",
|
||||
);
|
||||
// Explicit native Tab default emulation, not a browser tab-order claim.
|
||||
const outside = document.createElement("button");
|
||||
document.body.append(outside);
|
||||
if (!event.defaultPrevented) outside.focus();
|
||||
assert.equal(document.activeElement === outside, true, "native fallback departs editor");
|
||||
assert.equal(
|
||||
document.activeElement === outside,
|
||||
true,
|
||||
"native fallback departs editor",
|
||||
);
|
||||
editor.focus();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2343,9 +2343,7 @@ test("navigating to mention Options during a held selection inserts nothing late
|
||||
await expect(input).toBeFocused();
|
||||
await page.keyboard.press("Shift+Tab");
|
||||
await expect(
|
||||
page
|
||||
.getByTestId("message-composer")
|
||||
.getByTestId("mention-options-trigger"),
|
||||
page.getByTestId("message-composer").getByTestId("mention-options-trigger"),
|
||||
).toBeFocused();
|
||||
|
||||
// Release the held authority response and settle the downstream DOM.
|
||||
@@ -2437,9 +2435,9 @@ test("navigating away from the mention row pin during its held revalidation pins
|
||||
await holdMentionGateCommand(page, "revalidate_relay_agents");
|
||||
await page.keyboard.press("Enter");
|
||||
await waitForMentionGate(page);
|
||||
await expect(
|
||||
page.getByTestId("composer-address-lock-status"),
|
||||
).toContainText("Checking access");
|
||||
await expect(page.getByTestId("composer-address-lock-status")).toContainText(
|
||||
"Checking access",
|
||||
);
|
||||
|
||||
// Native Shift+Tab from the pin — the editor's key handler sees no overlay
|
||||
// events — must really move focus off the pin control.
|
||||
@@ -2510,120 +2508,136 @@ test("navigating away from the mention row pin during its held revalidation pins
|
||||
`composer-address-lock-${ALLOWLIST_RELAY_AGENT_PUBKEY}`,
|
||||
),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByTestId("composer-address-lock-status"),
|
||||
).toContainText("Automatically mentioning quinn");
|
||||
await expect(page.getByTestId("composer-address-lock-status")).toContainText(
|
||||
"Automatically mentioning quinn",
|
||||
);
|
||||
await expect(input).toHaveText("@quinn ");
|
||||
await expect(input.locator(".agent-mention-highlight")).toHaveText("quinn");
|
||||
});
|
||||
|
||||
test("editing and restoring the draft during a held mention selection inserts nothing late", async ({
|
||||
page,
|
||||
}) => {
|
||||
await installMockBridge(page, {
|
||||
relayAgents: [
|
||||
{
|
||||
pubkey: ALLOWLIST_RELAY_AGENT_PUBKEY,
|
||||
name: "quinn",
|
||||
respondTo: "allowlist",
|
||||
respondToAllowlist: [MOCK_VIEWER_PUBKEY],
|
||||
channelNames: ["general"],
|
||||
},
|
||||
],
|
||||
});
|
||||
await page.goto("/");
|
||||
await page.getByTestId("channel-general").click();
|
||||
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
||||
|
||||
const composer = page.getByTestId("message-composer");
|
||||
const input = page.getByTestId("message-input");
|
||||
// Read-only DOM offsets: no editor state or browser selection is changed.
|
||||
const readCaret = () =>
|
||||
input.evaluate((element) => {
|
||||
const selection = window.getSelection();
|
||||
if (
|
||||
!selection?.anchorNode ||
|
||||
!selection.focusNode ||
|
||||
!element.contains(selection.anchorNode) ||
|
||||
!element.contains(selection.focusNode)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const offset = (node: Node, position: number) => {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(element);
|
||||
range.setEnd(node, position);
|
||||
return range.toString().length;
|
||||
};
|
||||
return {
|
||||
anchor: offset(selection.anchorNode, selection.anchorOffset),
|
||||
focus: offset(selection.focusNode, selection.focusOffset),
|
||||
collapsed: selection.isCollapsed,
|
||||
};
|
||||
for (const change of ["draft", "selection range"] as const) {
|
||||
test(`editing and restoring the ${change} during a held mention selection inserts nothing late`, async ({
|
||||
page,
|
||||
}) => {
|
||||
await installMockBridge(page, {
|
||||
relayAgents: [
|
||||
{
|
||||
pubkey: ALLOWLIST_RELAY_AGENT_PUBKEY,
|
||||
name: "quinn",
|
||||
respondTo: "allowlist",
|
||||
respondToAllowlist: [MOCK_VIEWER_PUBKEY],
|
||||
channelNames: ["general"],
|
||||
},
|
||||
],
|
||||
});
|
||||
await input.fill("@quinn");
|
||||
const quinnRow = autocomplete(page).locator("button", { hasText: "quinn" });
|
||||
await expect(quinnRow).toBeVisible();
|
||||
await expect(quinnRow).toBeEnabled();
|
||||
await expect(input).toBeFocused();
|
||||
const originalCaret = await readCaret();
|
||||
expect(originalCaret).toEqual({ anchor: 6, focus: 6, collapsed: true });
|
||||
const baselineCommands = await readCommandLog(page);
|
||||
await page.goto("/");
|
||||
await page.getByTestId("channel-general").click();
|
||||
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
||||
|
||||
await holdMentionGateCommand(page, "revalidate_relay_agents");
|
||||
await quinnRow.click();
|
||||
await waitForMentionGate(page);
|
||||
const status = page.getByTestId("composer-address-lock-status");
|
||||
await expect(status).toContainText("Checking access");
|
||||
await expect(input).toBeFocused();
|
||||
await expect.poll(readCaret).toEqual(originalCaret);
|
||||
const composer = page.getByTestId("message-composer");
|
||||
const input = page.getByTestId("message-input");
|
||||
// Read-only DOM offsets: no editor state or browser selection is changed.
|
||||
const readCaret = () =>
|
||||
input.evaluate((element) => {
|
||||
const selection = window.getSelection();
|
||||
if (
|
||||
!selection?.anchorNode ||
|
||||
!selection.focusNode ||
|
||||
!element.contains(selection.anchorNode) ||
|
||||
!element.contains(selection.focusNode)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const offset = (node: Node, position: number) => {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(element);
|
||||
range.setEnd(node, position);
|
||||
return range.toString().length;
|
||||
};
|
||||
return {
|
||||
anchor: offset(selection.anchorNode, selection.anchorOffset),
|
||||
focus: offset(selection.focusNode, selection.focusOffset),
|
||||
collapsed: selection.isCollapsed,
|
||||
};
|
||||
});
|
||||
await input.fill("@quinn");
|
||||
const quinnRow = autocomplete(page).locator("button", { hasText: "quinn" });
|
||||
await expect(quinnRow).toBeVisible();
|
||||
await expect(quinnRow).toBeEnabled();
|
||||
await expect(input).toBeFocused();
|
||||
const originalCaret = await readCaret();
|
||||
expect(originalCaret).toEqual({ anchor: 6, focus: 6, collapsed: true });
|
||||
const baselineCommands = await readCommandLog(page);
|
||||
|
||||
// Real edits invalidate the pending operation even when text AND caret
|
||||
// return to their original values before the allowed response arrives.
|
||||
await page.keyboard.press("Backspace");
|
||||
await expect(input).toHaveText("@quin");
|
||||
await expect(input).toBeFocused();
|
||||
await page.keyboard.type("n");
|
||||
await expect(input).toHaveText("@quinn");
|
||||
await expect(input).toBeFocused();
|
||||
await expect.poll(readCaret).toEqual(originalCaret);
|
||||
await holdMentionGateCommand(page, "revalidate_relay_agents");
|
||||
await quinnRow.click();
|
||||
await waitForMentionGate(page);
|
||||
const status = page.getByTestId("composer-address-lock-status");
|
||||
await expect(status).toContainText("Checking access");
|
||||
await expect(input).toBeFocused();
|
||||
await expect.poll(readCaret).toEqual(originalCaret);
|
||||
|
||||
await releaseMentionGate(page);
|
||||
await expect
|
||||
.poll(async () =>
|
||||
commandCount(await readCommandLog(page), "revalidate_relay_agents"),
|
||||
)
|
||||
.toBe(commandCount(baselineCommands, "revalidate_relay_agents") + 1);
|
||||
// Same downstream settlement window as the adjacent held-gate probes.
|
||||
await page.waitForTimeout(300);
|
||||
if (change === "draft") {
|
||||
// Behavior coverage: text edits also invalidate the query revision.
|
||||
await page.keyboard.press("Backspace");
|
||||
await expect(input).toHaveText("@quin");
|
||||
await expect(input).toBeFocused();
|
||||
await page.keyboard.type("n");
|
||||
} else {
|
||||
// Query snapshots use the selection ANCHOR, not its moving head.
|
||||
// Extend and retract a real native range without changing text/anchor:
|
||||
// query revision cannot mask removal of editor cancellation listeners.
|
||||
await page.keyboard.press("Shift+ArrowLeft");
|
||||
await expect(input).toHaveText("@quinn");
|
||||
await expect(input).toBeFocused();
|
||||
await expect.poll(readCaret).toEqual({
|
||||
anchor: 6,
|
||||
focus: 5,
|
||||
collapsed: false,
|
||||
});
|
||||
await page.keyboard.press("Shift+ArrowRight");
|
||||
}
|
||||
await expect(input).toHaveText("@quinn");
|
||||
await expect(input).toBeFocused();
|
||||
await expect.poll(readCaret).toEqual(originalCaret);
|
||||
|
||||
await expect(input).toBeFocused();
|
||||
await expect(input).toHaveText("@quinn");
|
||||
await expect.poll(readCaret).toEqual(originalCaret);
|
||||
await expect(input.locator(".mention-chip")).toHaveCount(0);
|
||||
await expect(input.locator(".agent-mention-highlight")).toHaveCount(0);
|
||||
await expect(
|
||||
composer.getByTestId(
|
||||
`composer-address-lock-${ALLOWLIST_RELAY_AGENT_PUBKEY}`,
|
||||
),
|
||||
).toHaveCount(0);
|
||||
await expect(status).not.toContainText("Checking access");
|
||||
await expect(status).not.toContainText("Automatically mentioning");
|
||||
const settledCommands = await readCommandLog(page);
|
||||
for (const command of [
|
||||
"add_channel_members",
|
||||
"start_managed_agent",
|
||||
"attach_managed_agent",
|
||||
"sync_agents_to_active_huddle",
|
||||
"send_channel_message",
|
||||
"sign_event",
|
||||
]) {
|
||||
expect(commandCount(settledCommands, command)).toBe(
|
||||
commandCount(baselineCommands, command),
|
||||
);
|
||||
}
|
||||
expect(await readOutgoingMentionPubkeys(page, "@quinn")).toBeNull();
|
||||
});
|
||||
await releaseMentionGate(page);
|
||||
await expect
|
||||
.poll(async () =>
|
||||
commandCount(await readCommandLog(page), "revalidate_relay_agents"),
|
||||
)
|
||||
.toBe(commandCount(baselineCommands, "revalidate_relay_agents") + 1);
|
||||
// Same downstream settlement window as the adjacent held-gate probes.
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
await expect(input).toBeFocused();
|
||||
await expect(input).toHaveText("@quinn");
|
||||
await expect.poll(readCaret).toEqual(originalCaret);
|
||||
await expect(input.locator(".mention-chip")).toHaveCount(0);
|
||||
await expect(input.locator(".agent-mention-highlight")).toHaveCount(0);
|
||||
await expect(
|
||||
composer.getByTestId(
|
||||
`composer-address-lock-${ALLOWLIST_RELAY_AGENT_PUBKEY}`,
|
||||
),
|
||||
).toHaveCount(0);
|
||||
await expect(status).not.toContainText("Checking access");
|
||||
await expect(status).not.toContainText("Automatically mentioning");
|
||||
const settledCommands = await readCommandLog(page);
|
||||
for (const command of [
|
||||
"add_channel_members",
|
||||
"start_managed_agent",
|
||||
"attach_managed_agent",
|
||||
"sync_agents_to_active_huddle",
|
||||
"send_channel_message",
|
||||
"sign_event",
|
||||
]) {
|
||||
expect(commandCount(settledCommands, command)).toBe(
|
||||
commandCount(baselineCommands, command),
|
||||
);
|
||||
}
|
||||
expect(await readOutgoingMentionPubkeys(page, "@quinn")).toBeNull();
|
||||
});
|
||||
}
|
||||
|
||||
test("selected relay agents are invited as bots before sending", async ({
|
||||
page,
|
||||
|
||||
Reference in New Issue
Block a user