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
459 lines
20 KiB
JavaScript
459 lines
20 KiB
JavaScript
// Facebook notifications — unit tests for pure helpers + JSDOM fixture
|
|
// test for the `extractNotificationRowsFromDoc` extractor.
|
|
//
|
|
// The fixture (`__fixtures__/notifications-page.html`) is a frozen
|
|
// snapshot of `www.facebook.com/notifications` captured live from a
|
|
// logged-in session. Class names are stripped; SVG / `<i>` icon nodes
|
|
// are removed. The structure preserves what the extractor needs:
|
|
// - 5 `[role="listitem"]` rows: 1 header ("新通知" — no anchor) +
|
|
// 4 real notifications across 3 distinct `notif_t` types
|
|
// (`onthisday`, `approve_from_another_device`,
|
|
// `group_recommendation`).
|
|
// - Each data row has `<a href>` with `notif_id` + `notif_t` query
|
|
// params, `<div>未读</div>` unread badge, `<abbr aria-label="N天前">
|
|
// <span>N天</span></abbr>` for time, and a `<div role="button"
|
|
// aria-label="标记为已读,<body>">` for the bare body text.
|
|
//
|
|
// Per dianping #1313 / hupu #1387 / xiaoe #1388 pattern: the live IIFE
|
|
// embeds the same `extractNotificationRowsFromDoc` function via
|
|
// `${fn.toString()}` so the extractor seen by these JSDOM tests is the
|
|
// exact same code that runs in the browser.
|
|
|
|
import { describe, it, expect, vi } from 'vitest';
|
|
import { JSDOM } from 'jsdom';
|
|
import { readFileSync } from 'node:fs';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { dirname, resolve } from 'node:path';
|
|
import {
|
|
ArgumentError,
|
|
AuthRequiredError,
|
|
CommandExecutionError,
|
|
EmptyResultError,
|
|
} from '@jackwener/opencli/errors';
|
|
|
|
import {
|
|
FB_HOST,
|
|
NOTIFICATIONS_LIMIT_DEFAULT,
|
|
NOTIFICATIONS_LIMIT_MAX,
|
|
MARK_AS_READ_PREFIXES,
|
|
UNREAD_BADGE_LABELS,
|
|
normalizeNotificationsLimit,
|
|
stripMarkAsReadPrefix,
|
|
stripAnchorChrome,
|
|
parseNotifQuery,
|
|
isFacebookAuthRedirectPath,
|
|
extractNotificationRowsFromDoc,
|
|
buildNotificationsScript,
|
|
notificationsCommand,
|
|
} from './notifications.js';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = dirname(__filename);
|
|
const fixturePath = resolve(__dirname, '__fixtures__/notifications-page.html');
|
|
const fixtureHtml = readFileSync(fixturePath, 'utf8');
|
|
const manifestPath = resolve(__dirname, '../../cli-manifest.json');
|
|
|
|
function loadFixtureDoc() {
|
|
return new JSDOM(fixtureHtml, { url: 'https://www.facebook.com/notifications' }).window.document;
|
|
}
|
|
|
|
function loadManifestCommand() {
|
|
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
return manifest.find(cmd => cmd.site === 'facebook' && cmd.name === 'notifications');
|
|
}
|
|
|
|
const fixtureHelpers = {
|
|
stripMark: stripMarkAsReadPrefix,
|
|
stripChrome: stripAnchorChrome,
|
|
parseQuery: parseNotifQuery,
|
|
fbHost: FB_HOST,
|
|
markPrefixes: MARK_AS_READ_PREFIXES,
|
|
unreadBadges: UNREAD_BADGE_LABELS,
|
|
};
|
|
|
|
describe('facebook/notifications — registration contract', () => {
|
|
it('declares site/name/access/strategy/browser correctly', () => {
|
|
expect(notificationsCommand.site).toBe('facebook');
|
|
expect(notificationsCommand.name).toBe('notifications');
|
|
expect(notificationsCommand.access).toBe('read');
|
|
expect(notificationsCommand.strategy).toBe('cookie');
|
|
expect(notificationsCommand.browser).toBe(true);
|
|
expect(notificationsCommand.navigateBefore).toBe(false);
|
|
});
|
|
|
|
it('exposes the seven enrichment columns in stable order', () => {
|
|
expect(notificationsCommand.columns).toEqual([
|
|
'index',
|
|
'unread',
|
|
'text',
|
|
'time',
|
|
'url',
|
|
'notif_id',
|
|
'notif_type',
|
|
]);
|
|
});
|
|
|
|
it('declares limit arg with default and help string', () => {
|
|
const limit = notificationsCommand.args.find(a => a.name === 'limit');
|
|
expect(limit).toBeDefined();
|
|
expect(limit.type).toBe('int');
|
|
expect(limit.default).toBe(NOTIFICATIONS_LIMIT_DEFAULT);
|
|
expect(limit.help).toContain(String(NOTIFICATIONS_LIMIT_MAX));
|
|
});
|
|
|
|
it('build manifest preserves navigateBefore=false so invalid limits do not pre-nav', () => {
|
|
const manifestCommand = loadManifestCommand();
|
|
expect(manifestCommand).toBeDefined();
|
|
expect(manifestCommand.navigateBefore).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('normalizeNotificationsLimit', () => {
|
|
it('returns default for undefined / null / empty', () => {
|
|
expect(normalizeNotificationsLimit(undefined)).toBe(NOTIFICATIONS_LIMIT_DEFAULT);
|
|
expect(normalizeNotificationsLimit(null)).toBe(NOTIFICATIONS_LIMIT_DEFAULT);
|
|
expect(normalizeNotificationsLimit('')).toBe(NOTIFICATIONS_LIMIT_DEFAULT);
|
|
});
|
|
|
|
it('accepts positive integer in range', () => {
|
|
expect(normalizeNotificationsLimit(1)).toBe(1);
|
|
expect(normalizeNotificationsLimit(50)).toBe(50);
|
|
expect(normalizeNotificationsLimit(NOTIFICATIONS_LIMIT_MAX)).toBe(NOTIFICATIONS_LIMIT_MAX);
|
|
expect(normalizeNotificationsLimit('25')).toBe(25);
|
|
});
|
|
|
|
it('rejects 0 / negative / over-max / non-integer / non-numeric — never silent clamps', () => {
|
|
expect(() => normalizeNotificationsLimit(0)).toThrow(/positive integer/);
|
|
expect(() => normalizeNotificationsLimit(-1)).toThrow(/positive integer/);
|
|
expect(() => normalizeNotificationsLimit(NOTIFICATIONS_LIMIT_MAX + 1)).toThrow(
|
|
new RegExp(`\\[1, ${NOTIFICATIONS_LIMIT_MAX}\\]`),
|
|
);
|
|
expect(() => normalizeNotificationsLimit(1.5)).toThrow(/positive integer/);
|
|
expect(() => normalizeNotificationsLimit('abc')).toThrow(/positive integer/);
|
|
});
|
|
});
|
|
|
|
describe('stripMarkAsReadPrefix', () => {
|
|
it('strips known locale prefixes', () => {
|
|
expect(stripMarkAsReadPrefix('标记为已读,回顾你在 2019年6月的那段过往.', MARK_AS_READ_PREFIXES))
|
|
.toBe('回顾你在 2019年6月的那段过往.');
|
|
expect(stripMarkAsReadPrefix('Mark as read, John liked your photo', MARK_AS_READ_PREFIXES))
|
|
.toBe('John liked your photo');
|
|
expect(stripMarkAsReadPrefix('Mark as Read, John liked your photo', MARK_AS_READ_PREFIXES))
|
|
.toBe('John liked your photo');
|
|
});
|
|
|
|
it('returns null when prefix is missing — never silent default', () => {
|
|
expect(stripMarkAsReadPrefix('管理Jake Win通知设置', MARK_AS_READ_PREFIXES)).toBeNull();
|
|
expect(stripMarkAsReadPrefix('arbitrary aria-label without prefix', MARK_AS_READ_PREFIXES))
|
|
.toBeNull();
|
|
});
|
|
|
|
it('returns null on missing / empty input', () => {
|
|
expect(stripMarkAsReadPrefix(null, MARK_AS_READ_PREFIXES)).toBeNull();
|
|
expect(stripMarkAsReadPrefix(undefined, MARK_AS_READ_PREFIXES)).toBeNull();
|
|
expect(stripMarkAsReadPrefix('', MARK_AS_READ_PREFIXES)).toBeNull();
|
|
expect(stripMarkAsReadPrefix(' ', MARK_AS_READ_PREFIXES)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('stripAnchorChrome', () => {
|
|
it('strips leading 未读 / Unread badge', () => {
|
|
expect(stripAnchorChrome('未读John liked your photo', null, UNREAD_BADGE_LABELS))
|
|
.toBe('John liked your photo');
|
|
expect(stripAnchorChrome('UnreadJohn liked your photo', null, UNREAD_BADGE_LABELS))
|
|
.toBe('John liked your photo');
|
|
});
|
|
|
|
it('strips trailing time-ago suffix when timeStr is provided', () => {
|
|
expect(stripAnchorChrome('未读回顾你在 2019年6月的那段过往.2天', '2天', UNREAD_BADGE_LABELS))
|
|
.toBe('回顾你在 2019年6月的那段过往');
|
|
expect(stripAnchorChrome('未读有人尝试登录,但我们已阻止。7周', '7周', UNREAD_BADGE_LABELS))
|
|
.toBe('有人尝试登录,但我们已阻止');
|
|
});
|
|
|
|
it('keeps text intact when there is no badge / no time', () => {
|
|
expect(stripAnchorChrome('John liked your photo', null, UNREAD_BADGE_LABELS))
|
|
.toBe('John liked your photo');
|
|
});
|
|
|
|
it('returns empty string for falsy input', () => {
|
|
expect(stripAnchorChrome('', null, UNREAD_BADGE_LABELS)).toBe('');
|
|
expect(stripAnchorChrome(null, null, UNREAD_BADGE_LABELS)).toBe('');
|
|
});
|
|
});
|
|
|
|
describe('parseNotifQuery', () => {
|
|
it('extracts notif_id and notif_t from absolute URLs', () => {
|
|
const href = 'https://www.facebook.com/photo/?fbid=104&set=a.105¬if_id=1777¬if_t=onthisday&ref=notif';
|
|
expect(parseNotifQuery(href, FB_HOST)).toEqual({
|
|
notif_id: '1777',
|
|
notif_type: 'onthisday',
|
|
});
|
|
});
|
|
|
|
it('handles relative URLs by resolving against fbHost', () => {
|
|
expect(parseNotifQuery('/groups/discover/?notif_id=1234¬if_t=group_recommendation', FB_HOST))
|
|
.toEqual({ notif_id: '1234', notif_type: 'group_recommendation' });
|
|
});
|
|
|
|
it('returns null fields when query params are absent — typed unknown', () => {
|
|
expect(parseNotifQuery('https://www.facebook.com/photo/?fbid=999', FB_HOST))
|
|
.toEqual({ notif_id: null, notif_type: null });
|
|
});
|
|
|
|
it('returns null fields for empty / unparseable input', () => {
|
|
expect(parseNotifQuery('', FB_HOST)).toEqual({ notif_id: null, notif_type: null });
|
|
expect(parseNotifQuery(null, FB_HOST)).toEqual({ notif_id: null, notif_type: null });
|
|
// jsdom's URL constructor accepts a lot — pass an obviously broken
|
|
// protocol to force the catch path.
|
|
expect(parseNotifQuery('http://[::not-an-ipv6', FB_HOST))
|
|
.toEqual({ notif_id: null, notif_type: null });
|
|
});
|
|
});
|
|
|
|
describe('isFacebookAuthRedirectPath', () => {
|
|
it('matches Facebook login/checkpoint paths including .php redirects', () => {
|
|
expect(isFacebookAuthRedirectPath('/login')).toBe(true);
|
|
expect(isFacebookAuthRedirectPath('/login/')).toBe(true);
|
|
expect(isFacebookAuthRedirectPath('/login.php')).toBe(true);
|
|
expect(isFacebookAuthRedirectPath('/login/identify/')).toBe(true);
|
|
expect(isFacebookAuthRedirectPath('/checkpoint')).toBe(true);
|
|
expect(isFacebookAuthRedirectPath('/checkpoint.php')).toBe(true);
|
|
});
|
|
|
|
it('does not match unrelated login-looking paths', () => {
|
|
expect(isFacebookAuthRedirectPath('/loginhelp')).toBe(false);
|
|
expect(isFacebookAuthRedirectPath('/help/login')).toBe(false);
|
|
expect(isFacebookAuthRedirectPath('/notifications')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('extractNotificationRowsFromDoc — JSDOM against frozen fixture', () => {
|
|
it('extracts 4 data rows (1 header skipped) with full schema and stable order', () => {
|
|
const doc = loadFixtureDoc();
|
|
const rows = extractNotificationRowsFromDoc(doc, 100, fixtureHelpers);
|
|
expect(rows).toHaveLength(4);
|
|
expect(rows[0]).toEqual({
|
|
index: 1,
|
|
unread: true,
|
|
text: '回顾你在 2019年6月的那段过往',
|
|
time: '2天',
|
|
url: 'https://www.facebook.com/photo/?fbid=104250644186433&set=a.104250310853133¬if_id=1777926497886652¬if_t=onthisday&ref=notif',
|
|
notif_id: '1777926497886652',
|
|
notif_type: 'onthisday',
|
|
});
|
|
expect(rows[2]).toMatchObject({
|
|
index: 3,
|
|
unread: true,
|
|
text: '有人尝试登录,但我们已阻止',
|
|
time: '7周',
|
|
notif_type: 'approve_from_another_device',
|
|
notif_id: '1773680546395691',
|
|
});
|
|
expect(rows[3]).toMatchObject({
|
|
index: 4,
|
|
unread: true,
|
|
text: '你可能会喜欢 ETYCAL VIBEZ fan\'s page',
|
|
time: '2天',
|
|
notif_type: 'group_recommendation',
|
|
});
|
|
});
|
|
|
|
it('respects limit by returning at most N rows', () => {
|
|
const doc = loadFixtureDoc();
|
|
const rows = extractNotificationRowsFromDoc(doc, 2, fixtureHelpers);
|
|
expect(rows).toHaveLength(2);
|
|
expect(rows.map(r => r.index)).toEqual([1, 2]);
|
|
});
|
|
|
|
it('skips the header listitem (no <a href>) — guards against silent empty', () => {
|
|
const doc = loadFixtureDoc();
|
|
const allListitems = doc.querySelectorAll('[role="listitem"]');
|
|
expect(allListitems.length).toBe(5); // 1 header + 4 data
|
|
const rows = extractNotificationRowsFromDoc(doc, 100, fixtureHelpers);
|
|
expect(rows.length).toBe(4); // header skipped
|
|
// None of the returned rows match the header text.
|
|
for (const row of rows) {
|
|
expect(row.text).not.toBe('新通知');
|
|
}
|
|
});
|
|
|
|
it('text column has no truncation — bug fix vs legacy substring(0, 150)', () => {
|
|
const doc = loadFixtureDoc();
|
|
const rows = extractNotificationRowsFromDoc(doc, 100, fixtureHelpers);
|
|
// The on-this-day row body is 16+ chars; the legacy adapter would
|
|
// have happily returned anything <= 150. The point of this test
|
|
// is to assert nothing is silently sliced — full body text round-
|
|
// trips through the extractor.
|
|
expect(rows[0].text).toBe('回顾你在 2019年6月的那段过往');
|
|
expect(rows[1].text).toBe('看看你在 2019年6月发布的帖子,瞬间回到过去的美好时光');
|
|
// No row has a value that ends with our legacy truncation point.
|
|
for (const row of rows) {
|
|
expect(row.text).not.toMatch(/\u2026$/); // U+2026 ellipsis as a defensive check
|
|
}
|
|
});
|
|
|
|
it('time is null (typed unknown) when abbr is missing — never the legacy "-" sentinel', () => {
|
|
// Build a one-off JSDOM doc that mimics a row without an abbr.
|
|
const html = `<!doctype html><html><body><div role="main">
|
|
<div role="listitem">
|
|
<a href="https://www.facebook.com/page?notif_id=999¬if_t=test_event">
|
|
<span><div>未读</div>Body text without time</span>
|
|
</a>
|
|
</div>
|
|
</body></html>`;
|
|
const doc = new JSDOM(html).window.document;
|
|
const rows = extractNotificationRowsFromDoc(doc, 5, fixtureHelpers);
|
|
expect(rows).toHaveLength(1);
|
|
expect(rows[0].time).toBeNull();
|
|
expect(rows[0].text).toBe('Body text without time');
|
|
});
|
|
|
|
it('unread is false when no badge is present — bug fix vs legacy column-drop', () => {
|
|
const html = `<!doctype html><html><body><div role="main">
|
|
<div role="listitem">
|
|
<a href="https://www.facebook.com/x?notif_id=1¬if_t=read">
|
|
<span>Already read notification</span>
|
|
<abbr aria-label="3 hr"><span>3 hr</span></abbr>
|
|
</a>
|
|
</div>
|
|
</body></html>`;
|
|
const doc = new JSDOM(html).window.document;
|
|
const rows = extractNotificationRowsFromDoc(doc, 5, fixtureHelpers);
|
|
expect(rows).toHaveLength(1);
|
|
expect(rows[0].unread).toBe(false);
|
|
expect(rows[0].text).toBe('Already read notification');
|
|
});
|
|
|
|
it('resolves relative hrefs to full URLs for the url column', () => {
|
|
const html = `<!doctype html><html><body><div role="main">
|
|
<div role="listitem">
|
|
<a href="/groups/discover/?notif_id=42¬if_t=group_recommendation">
|
|
<span>UnreadRelative URL body</span>
|
|
<abbr aria-label="1 hr"><span>1 hr</span></abbr>
|
|
</a>
|
|
</div>
|
|
</body></html>`;
|
|
const doc = new JSDOM(html, { url: 'https://www.facebook.com/notifications' }).window.document;
|
|
const rows = extractNotificationRowsFromDoc(doc, 5, fixtureHelpers);
|
|
expect(rows).toHaveLength(1);
|
|
expect(rows[0].url).toBe('https://www.facebook.com/groups/discover/?notif_id=42¬if_t=group_recommendation');
|
|
expect(rows[0].notif_id).toBe('42');
|
|
expect(rows[0].notif_type).toBe('group_recommendation');
|
|
});
|
|
|
|
it('skips anchor rows with no recoverable body text — no text:null success rows', () => {
|
|
const html = `<!doctype html><html><body><div role="main">
|
|
<div role="listitem">
|
|
<a href="https://www.facebook.com/x?notif_id=1¬if_t=blank">
|
|
<span><div>未读</div></span>
|
|
<abbr aria-label="3 hr"><span>3 hr</span></abbr>
|
|
</a>
|
|
</div>
|
|
</body></html>`;
|
|
const doc = new JSDOM(html).window.document;
|
|
const rows = extractNotificationRowsFromDoc(doc, 5, fixtureHelpers);
|
|
expect(rows).toEqual([]);
|
|
});
|
|
|
|
it('returns [] for a doc with no listitems — Node side maps to EmptyResultError', () => {
|
|
const doc = new JSDOM('<!doctype html><html><body></body></html>').window.document;
|
|
const rows = extractNotificationRowsFromDoc(doc, 5, fixtureHelpers);
|
|
expect(rows).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('buildNotificationsScript — IIFE invariants', () => {
|
|
it('embeds all four pure helpers via fn.toString()', () => {
|
|
const script = buildNotificationsScript(15);
|
|
expect(script).toContain('function stripMarkAsReadPrefix');
|
|
expect(script).toContain('function stripAnchorChrome');
|
|
expect(script).toContain('function parseNotifQuery');
|
|
expect(script).toContain('function isFacebookAuthRedirectPath');
|
|
expect(script).toContain('function extractNotificationRowsFromDoc');
|
|
});
|
|
|
|
it('inlines limit and FB_HOST so the live page does not depend on Node closures', () => {
|
|
const script = buildNotificationsScript(7);
|
|
expect(script).toMatch(/extractNotificationRowsFromDoc\(document,\s*7,/);
|
|
expect(script).toContain('"https://www.facebook.com"');
|
|
});
|
|
|
|
it('inlines locale prefix and badge tables so the IIFE has them at runtime', () => {
|
|
const script = buildNotificationsScript(15);
|
|
expect(script).toContain('"标记为已读,"');
|
|
expect(script).toContain('"未读"');
|
|
expect(script).toContain('"Unread"');
|
|
});
|
|
|
|
it('contains an auth-redirect guard before the DOM walk', () => {
|
|
const script = buildNotificationsScript(15);
|
|
expect(script).toMatch(/AUTH_REQUIRED.*facebook/i);
|
|
expect(script).toContain('isFacebookAuthRedirectPath(window.location.pathname');
|
|
});
|
|
|
|
it('does NOT contain the legacy silent-truncation slice/substring — anti-pattern regression guard', () => {
|
|
const script = buildNotificationsScript(15);
|
|
// Legacy: text.substring(0, 150) — silent-bad-shape that this PR fixes.
|
|
expect(script).not.toMatch(/text\.substring\(0,\s*150\)/);
|
|
// Legacy: time || '-' — silent sentinel that this PR fixes.
|
|
expect(script).not.toMatch(/time\s*\|\|\s*['"]-['"]/);
|
|
});
|
|
});
|
|
|
|
describe('facebook/notifications — func typed boundaries', () => {
|
|
function createPageMock(rows) {
|
|
return {
|
|
goto: vi.fn().mockResolvedValue(undefined),
|
|
evaluate: vi.fn().mockResolvedValue(rows),
|
|
};
|
|
}
|
|
|
|
function createFailingPageMock(error, { failGoto = false } = {}) {
|
|
return {
|
|
goto: vi.fn(failGoto ? () => Promise.reject(error) : () => Promise.resolve()),
|
|
evaluate: vi.fn(failGoto ? () => Promise.resolve([]) : () => Promise.reject(error)),
|
|
};
|
|
}
|
|
|
|
it('validates --limit upfront before navigation', async () => {
|
|
const page = createPageMock([]);
|
|
await expect(notificationsCommand.func(page, { limit: 0 })).rejects.toThrow(ArgumentError);
|
|
await expect(notificationsCommand.func(page, { limit: NOTIFICATIONS_LIMIT_MAX + 1 })).rejects.toThrow(ArgumentError);
|
|
expect(page.goto).not.toHaveBeenCalled();
|
|
expect(page.evaluate).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('returns rows verbatim on success', async () => {
|
|
const row = {
|
|
index: 1,
|
|
unread: true,
|
|
text: 'hello',
|
|
time: '2天',
|
|
url: 'https://www.facebook.com/notifications/?notif_id=1¬if_t=test',
|
|
notif_id: '1',
|
|
notif_type: 'test',
|
|
};
|
|
await expect(notificationsCommand.func(createPageMock([row]), { limit: 1 })).resolves.toEqual([row]);
|
|
});
|
|
|
|
it('maps empty rows to EmptyResultError', async () => {
|
|
await expect(notificationsCommand.func(createPageMock([]), { limit: 1 })).rejects.toThrow(EmptyResultError);
|
|
});
|
|
|
|
it('maps auth sentinel evaluate failures to AuthRequiredError', async () => {
|
|
const page = createFailingPageMock(new Error('AUTH_REQUIRED: facebook.com redirected to login'));
|
|
await expect(notificationsCommand.func(page, { limit: 1 })).rejects.toThrow(AuthRequiredError);
|
|
});
|
|
|
|
it('wraps navigation and evaluate failures as CommandExecutionError', async () => {
|
|
await expect(
|
|
notificationsCommand.func(createFailingPageMock(new Error('network down'), { failGoto: true }), { limit: 1 }),
|
|
).rejects.toThrow(CommandExecutionError);
|
|
await expect(
|
|
notificationsCommand.func(createFailingPageMock(new Error('selector crashed')), { limit: 1 }),
|
|
).rejects.toThrow(CommandExecutionError);
|
|
});
|
|
});
|