Files
wehub-resource-sync 9b395f5cc3
E2E Headed Chrome / e2e-headed (macos-15) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (ubuntu-latest) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (windows-latest) (push) Has been cancelled
CI / build (macos-latest) (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / unit-test (push) Has been cancelled
CI / bun-test (push) Has been cancelled
CI / adapter-test (push) Has been cancelled
CI / smoke-test (macos-latest) (push) Has been cancelled
CI / smoke-test (ubuntu-latest) (push) Has been cancelled
Security Audit / audit (push) Has been cancelled
Build Chrome Extension / build (push) Has been cancelled
Trigger Website Rebuild (Docs Updated) / dispatch (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:39:48 +08:00

165 lines
7.8 KiB
JavaScript

import { describe, expect, it, vi } from 'vitest';
import { JSDOM } from 'jsdom';
import { ArgumentError, CommandExecutionError } from '@jackwener/opencli/errors';
import { getRegistry } from '@jackwener/opencli/registry';
import { __test__ } from './delete.js';
describe('twitter delete command', () => {
it('targets the matched tweet article instead of the first More button on the page', async () => {
const cmd = getRegistry().get('twitter/delete');
expect(cmd?.func).toBeTypeOf('function');
const page = {
goto: vi.fn().mockResolvedValue(undefined),
wait: vi.fn().mockResolvedValue(undefined),
evaluate: vi.fn().mockResolvedValue({ ok: true, message: 'Tweet successfully deleted.' }),
};
const result = await cmd.func(page, {
url: 'https://x.com/alice/status/2040254679301718161?s=20',
});
expect(page.goto).toHaveBeenCalledWith('https://x.com/alice/status/2040254679301718161?s=20');
expect(page.wait).toHaveBeenNthCalledWith(1, { selector: '[data-testid="primaryColumn"]' });
expect(page.wait).toHaveBeenNthCalledWith(2, 2);
const script = page.evaluate.mock.calls[0][0];
// Article-scoping must come from the shared helper (not an inline
// `pathname.includes('/status/' + tweetId)` substring match — see
// codex-mini0 #1400 catch where `/status/123` would match
// `/status/1234567`). The helper emits `__twHasLinkToTarget` and
// `__twGetStatusIdFromHref` plus the canonical anchored regex.
expect(script).toContain('__twHasLinkToTarget');
expect(script).toContain('__twGetStatusIdFromHref');
expect(script).toContain("document.querySelectorAll('article')");
expect(script).toContain("targetArticle.querySelectorAll('button,[role=\"button\"]')");
expect(script).toContain("closest('article') === targetArticle");
expect(script).toContain(".filter(belongsToTargetArticle)");
// Localized "More" caret: prefer the language-agnostic data-testid, fall
// back to a multilingual aria-label match (zh-Hans 更多), and poll for the
// late-hydrating target article before giving up.
expect(script).toContain('[data-testid="caret"]');
expect(script).toContain('/^(More|更多)/');
expect(script).toContain('i < 20');
// Delete menu item is localized (删除) and must exclude the Lists item in
// both languages (List / 列表).
expect(script).toContain('删除');
expect(script).toContain('列表');
// Substring match must NOT appear — exact-id match only.
expect(script).not.toContain("'/status/' + tweetId");
expect(result).toEqual([
{
status: 'success',
message: 'Tweet successfully deleted.',
},
]);
});
it('passes through matched-tweet lookup failures', async () => {
const cmd = getRegistry().get('twitter/delete');
expect(cmd?.func).toBeTypeOf('function');
const page = {
goto: vi.fn().mockResolvedValue(undefined),
wait: vi.fn().mockResolvedValue(undefined),
evaluate: vi.fn().mockResolvedValue({
ok: false,
message: 'Could not find the tweet card matching the requested URL.',
}),
};
const result = await cmd.func(page, {
url: 'https://x.com/alice/status/2040254679301718161',
});
expect(result).toEqual([
{
status: 'failed',
message: 'Could not find the tweet card matching the requested URL.',
},
]);
expect(page.wait).toHaveBeenCalledTimes(1);
});
it('unwraps Browser Bridge evaluate envelopes before checking delete success', async () => {
const cmd = getRegistry().get('twitter/delete');
expect(cmd?.func).toBeTypeOf('function');
const page = {
goto: vi.fn().mockResolvedValue(undefined),
wait: vi.fn().mockResolvedValue(undefined),
evaluate: vi.fn().mockResolvedValue({
session: 'twitter',
data: { ok: true, message: 'Tweet successfully deleted.' },
}),
};
const result = await cmd.func(page, {
url: 'https://x.com/alice/status/2040254679301718161',
});
expect(result).toEqual([
{
status: 'success',
message: 'Tweet successfully deleted.',
},
]);
expect(page.wait).toHaveBeenNthCalledWith(2, 2);
});
it('ignores stale non-target delete menu items that existed before opening the matched tweet menu', async () => {
const dom = new JSDOM(`
<body>
<div role="menuitem" data-stale-delete>删除</div>
<article>
<a href="https://x.com/bob/status/999">wrong tweet</a>
<button data-testid="caret" aria-label="更多"></button>
</article>
<article>
<a href="https://x.com/alice/status/2040254679301718161">target tweet</a>
<button data-testid="caret" aria-label="更多" data-target-caret></button>
</article>
</body>
`, { runScripts: 'outside-only', url: 'https://x.com/alice/status/2040254679301718161' });
dom.window.setTimeout = (handler) => {
if (typeof handler === 'function') handler();
return 0;
};
Object.defineProperty(dom.window.HTMLElement.prototype, 'getClientRects', {
configurable: true,
value() {
return [{ bottom: 1, height: 1, left: 0, right: 1, top: 0, width: 1 }];
},
});
let staleDeleteClicked = false;
let targetCaretClicked = false;
dom.window.document.querySelector('[data-stale-delete]')?.addEventListener('click', () => {
staleDeleteClicked = true;
});
dom.window.document.querySelector('[data-target-caret]')?.addEventListener('click', () => {
targetCaretClicked = true;
const item = dom.window.document.createElement('div');
item.setAttribute('role', 'menuitem');
item.textContent = 'Pin to your profile';
dom.window.document.body.appendChild(item);
});
const result = await dom.window.eval(__test__.buildDeleteScript('2040254679301718161'));
expect(targetCaretClicked).toBe(true);
expect(staleDeleteClicked).toBe(false);
expect(result).toEqual({
ok: false,
message: 'The matched tweet menu did not contain Delete. This tweet may not belong to you.',
});
});
it('rejects malformed or off-domain URLs with ArgumentError before navigation', async () => {
const cmd = getRegistry().get('twitter/delete');
expect(cmd?.func).toBeTypeOf('function');
const page = {
goto: vi.fn(),
wait: vi.fn(),
evaluate: vi.fn(),
};
// parseTweetUrl bubbles ArgumentError directly (no CommandExecutionError
// wrapping); replaces the previous local extractTweetId path that hid
// typed-input failures behind a generic CliError.
await expect(cmd.func(page, {
url: 'https://x.com/alice/home',
})).rejects.toThrow(ArgumentError);
expect(page.goto).not.toHaveBeenCalled();
expect(page.wait).not.toHaveBeenCalled();
expect(page.evaluate).not.toHaveBeenCalled();
});
it('throws CommandExecutionError when no page is provided', async () => {
const cmd = getRegistry().get('twitter/delete');
await expect(cmd.func(undefined, {
url: 'https://x.com/alice/status/2040254679301718161',
})).rejects.toThrow(CommandExecutionError);
});
});