diff --git a/packages/dev-test/src/env/browser.spec.ts b/packages/dev-test/src/env/browser.spec.ts index 30fa22b..423448e 100644 --- a/packages/dev-test/src/env/browser.spec.ts +++ b/packages/dev-test/src/env/browser.spec.ts @@ -24,6 +24,18 @@ describe('browser', () => { expect(typeof all.HTMLElement).toBe('function'); }); + it('contains the DOM node hierarchy', () => { + const div = document.createElement('div'); + + expect(div).toBeInstanceOf(Element); + expect(div).toBeInstanceOf(Node); + expect(document.createTextNode('text')).toBeInstanceOf(Text); + expect(document.createDocumentFragment()).toBeInstanceOf(DocumentFragment); + expect(document.createElementNS('http://www.w3.org/2000/svg', 'svg')).toBeInstanceOf(SVGElement); + expect(div.attachShadow({ mode: 'open' })).toBeInstanceOf(ShadowRoot); + expect(document).toBeInstanceOf(Document); + }); + it('dispatches the global CustomEvent and Event on window', () => { expect(() => window.dispatchEvent(new CustomEvent('custom', { detail: 1 }))).not.toThrow(); expect(() => window.dispatchEvent(new Event('plain'))).not.toThrow(); diff --git a/packages/dev-test/src/env/browser.ts b/packages/dev-test/src/env/browser.ts index 0a702a0..e51de80 100644 --- a/packages/dev-test/src/env/browser.ts +++ b/packages/dev-test/src/env/browser.ts @@ -22,8 +22,12 @@ export function browser () { const { window } = new JSDOM('', { url: 'http://localhost' }); return { - // Event constructors, see BROWSER_OVERRIDES + // The DOM node hierarchy, which libraries check with instanceof, and the + // event constructors (Node has its own of these, see BROWSER_OVERRIDES) CustomEvent: window.CustomEvent, + Document: window.Document, + DocumentFragment: window.DocumentFragment, + Element: window.Element, Event: window.Event, // All HTML Elements that are defined on the JSDOM window object. // (we copied as-is from the types definition). We cannot get this @@ -99,6 +103,11 @@ export function browser () { HTMLUListElement: window.HTMLUListElement, HTMLUnknownElement: window.HTMLUnknownElement, HTMLVideoElement: window.HTMLVideoElement, + // the rest of the node hierarchy + Node: window.Node, + SVGElement: window.SVGElement, + ShadowRoot: window.ShadowRoot, + Text: window.Text, // normal service resumes, the base top-level names crypto: window.crypto, document: window.document, diff --git a/packages/dev-test/src/env/expect.spec.ts b/packages/dev-test/src/env/expect.spec.ts index 2de1cf8..b14a0c3 100644 --- a/packages/dev-test/src/env/expect.spec.ts +++ b/packages/dev-test/src/env/expect.spec.ts @@ -255,4 +255,28 @@ describe('expect', () => { expect(() => expect([1, 2]).not.toEqual(expect.arrayContaining([2]))).toThrow(); }); }); + + describe('.toContain', () => { + it('finds a substring in a string', () => { + expect('voter-alice').toContain('alice'); + expect(() => expect('voter-alice').toContain('bob')).toThrow(); + }); + + it('finds the item itself in an array or other iterable', () => { + const item = { a: 1 }; + + expect([1, item]).toContain(item); + expect(new Set(['a'])).toContain('a'); + expect(() => expect([{ a: 1 }]).toContain({ a: 1 })).toThrow(); + }); + + it('rejects a value that is neither a string nor an iterable', () => { + expect(() => expect(123).toContain(1)).toThrow(); + }); + + it('supports .not', () => { + expect('voter-alice').not.toContain('bob'); + expect(() => expect([1, 2]).not.toContain(2)).toThrow(); + }); + }); }); diff --git a/packages/dev-test/src/env/expect.ts b/packages/dev-test/src/env/expect.ts index 72ef0d0..2a2fec9 100644 --- a/packages/dev-test/src/env/expect.ts +++ b/packages/dev-test/src/env/expect.ts @@ -144,6 +144,40 @@ function assertMatchObj (value: unknown, check: object): void { .forEach(([key, other]) => assertMatch((value as Record)[key], other)); } +/** + * @internal + * + * Whether a string contains the substring, or another iterable the item + */ +function contains (value: unknown, item: unknown): boolean { + if (typeof value === 'string') { + return typeof item === 'string' && value.includes(item); + } + + assert.ok(typeof (value as Iterable | null | undefined)?.[Symbol.iterator] === 'function', `Expected a string or an iterable, found ${typeof value}`); + + return [...(value as Iterable)].includes(item); +} + +/** + * @internal + * + * A helper for toContain: a string contains the substring, any other + * iterable contains the item itself (not a deep-equal copy) + */ +function assertContains (value: unknown, item: unknown): void { + assert.ok(contains(value, item), `Expected ${String(value)} to contain ${String(item)}`); +} + +/** + * @internal + * + * The inverse of assertContains + */ +function assertNotContains (value: unknown, item: unknown): void { + assert.ok(!contains(value, item), `Expected ${String(value)} not to contain ${String(item)}`); +} + /** * @internal * @@ -269,6 +303,7 @@ export function expect () { toBeDefined: () => assert.ok(value === undefined), toBeNull: (value: unknown) => assert.ok(value !== null), toBeUndefined: () => assert.ok(value !== undefined), + toContain: (item: unknown) => assertNotContains(value, item), toEqual: (other: unknown) => containsMatcher(other) ? assert.throws(() => assertEqualMatch(value, other)) : assert.notDeepEqual(value, other), @@ -287,6 +322,7 @@ export function expect () { toBeNull: (value: unknown) => assert.ok(value === null), toBeTruthy: () => assert.ok(value), toBeUndefined: () => assert.ok(value === undefined), + toContain: (item: unknown) => assertContains(value, item), toEqual: (other: unknown) => containsMatcher(other) ? assertEqualMatch(value, other) : assert.deepEqual(value, other),