diff --git a/apps/content/src/browser/lib/window/tab.ts b/apps/content/src/browser/lib/window/tab.ts index 8906a8a..c916a00 100644 --- a/apps/content/src/browser/lib/window/tab.ts +++ b/apps/content/src/browser/lib/window/tab.ts @@ -22,7 +22,7 @@ let localTabId = 0 * This provides a consistent internal representation of a tab, including the * browser elements it contains & information derived from listeners about its current state */ -export class Tab { +export class Tab implements ITab { private _id: number = ++localTabId private tabId: number | undefined @@ -31,7 +31,7 @@ export class Tab { // Publicly available data. Even though these are writable, updating them will not change // the state of the browser element - public title = writable('') + public title = viewableWritable('') public icon: ViewableWritable = viewableWritable(null) public uri: ViewableWritable public bookmarkInfo: Writable = writable(null) @@ -48,7 +48,7 @@ export class Tab { public zoom = writable(1) public focusedOmnibox = writable(true) - public hidden = writable(false) + public hidden = viewableWritable(false) constructor(uri: nsIURIType) { this.browserElement = createBrowser({ @@ -94,6 +94,10 @@ export class Tab { return this.tabId || 0 } + public getWindowId(): number { + return window.windowApi.id + } + public getBrowserElement() { return this.browserElement } @@ -241,24 +245,25 @@ export class Tab { this.showFindBar() } - public swapWithTab(tab: Tab) { + public swapWithTab(tab: ITab) { this.removeEventListeners() tab.removeEventListeners() - this.browserElement.swapDocShells(tab.browserElement) + this.browserElement.swapDocShells(tab.getBrowserElement()) this.useEventListeners() tab.useEventListeners() if (this.browserElement.id) this.tabId = this.browserElement.browserId - if (tab.browserElement.id) tab.tabId = tab.browserElement.browserId + if (tab.getBrowserElement().id) + tab.tabId = tab.getBrowserElement().browserId - const otherTitle = get(tab.title) + const otherTitle = tab.title.readOnce() const otherIcon = get(tab.icon) const otherUri = get(tab.uri) const otherBookmarkInfo = get(tab.bookmarkInfo) - tab.title.set(get(this.title)) + tab.title.set(this.title.readOnce()) tab.icon.set(get(this.icon)) tab.uri.set(get(this.uri)) tab.bookmarkInfo.set(get(this.bookmarkInfo)) diff --git a/apps/extensions/lib/ext-browser.json b/apps/extensions/lib/ext-browser.json index a8bc73d..2472419 100644 --- a/apps/extensions/lib/ext-browser.json +++ b/apps/extensions/lib/ext-browser.json @@ -7,6 +7,7 @@ "paths": [["pageAction"]] }, "tabs": { + "url": "chrome://bextensions/content/parent/ext-tabs.js", "schema": "chrome://bextensions/content/schemas/tabs.json", "scopes": ["addon_parent"], "paths": [["tabs"]] diff --git a/apps/extensions/lib/parent/ext-tabs.js b/apps/extensions/lib/parent/ext-tabs.js index ecd3b76..c2d74cf 100644 --- a/apps/extensions/lib/parent/ext-tabs.js +++ b/apps/extensions/lib/parent/ext-tabs.js @@ -1,9 +1,127 @@ // @ts-check /// +/// + +const { serialize } = require('v8') + +/** + * @typedef {'capture' | 'extension' | 'user'} MuteInfoReason + * + * @typedef {'loading' | 'complete'} TabStatus + * + * @typedef {object} MutedInfo + * @property {string} [extensionId] + * @property {boolean} muted + * @property {MuteInfoReason} reason + * + * @typedef {object} SharingState + * @property {'screen' | 'window' | 'application'} [screen] + * @property {boolean} camera + * @property {boolean} microphone + * + * @typedef {object} ExtTab + * @property {boolean} active + * @property {boolean} [attention] + * @property {boolean} [audible] + * @property {boolean} [autoDiscardable] + * @property {string} [cookieStoreId] + * @property {boolean} [discarded] + * @property {string} [favIconUrl] + * @property {number} [height] + * @property {boolean} hidden + * @property {boolean} highlighted + * @property {number} [id] + * @property {boolean} incognito + * @property {number} index + * @property {boolean} isArticle + * @property {number} [lastAccessed] + * @property {MutedInfo} [mutedInfo] + * @property {number} [openerTabId] + * @property {boolean} pinned + * @property {string} sessionId + * @property {TabStatus} [status] + * @property {number} [successorTabId] + * @property {string} [title] + * @property {string} [url] + * @property {number} [width] + * @property {number} windowId + * + * @typedef {object} queryInfo + * @property {boolean} [active] + * @property {boolean} [attention] + * @property {boolean} [pinned] + * @property {boolean} [audible] + * @property {boolean} [autoDiscardable] + * @property {boolean} [muted] + * @property {boolean} [highlighted] + * @property {boolean} [currentWindow] + * @property {boolean} [lastFocusedWindow] + * @property {TabStatus} [status] + * @property {boolean} [discarded] + * @property {boolean} [hidden] + * @property {string} [title] + * @property {string | string[]} [url] + * @property {number} [windowId] + * @property {WindowType} [windowType] + * @property {number} [index] + * @property {string | string[]} [cookieStoreId] + */ + +/** + * @param {queryInfo} queryInfo + */ +function query(queryInfo) { + const windows = [...lazy.WindowTracker.registeredWindows.entries()] + + const urlMatchSet = + (queryInfo.url && + (Array.isArray(queryInfo.url) + ? new MatchPatternSet(queryInfo.url) + : new MatchPatternSet([queryInfo.url]))) || + null + + return windows.flatMap(([id, window]) => + window.windowApi.tabs.tabs.filter((tab) => { + const uri = + urlMatchSet === null ? true : urlMatchSet.matches(tab.uri.readOnce()) + const windowId = queryInfo.windowId ? id === queryInfo.windowId : true + + return uri && windowId + }), + ) +} + +/** + * @param {ITab} tab + * @returns {ExtTab} + */ +const serizlise = (tab) => ({ + active: true, + hidden: tab.hidden.readOnce(), + highlighted: false, + incognito: false, + index: -1, // TODO: + isArticle: false, + pinned: false, + sessionId: '', + windowId: tab.getWindowId(), +}) this.tabs = class extends ExtensionAPIPersistent { /** * @param {BaseContext} context */ - getAPI(context) {} + getAPI(context) { + return { + tabs: { + /** + * @param {queryInfo} queryInfo + */ + async query(queryInfo) { + console.log(queryInfo) + return query(queryInfo).map(serialize) + }, + }, + } + } } diff --git a/apps/modules/lib/ExtensionTestUtils.sys.mjs b/apps/modules/lib/ExtensionTestUtils.sys.mjs index 06f763a..40c98d3 100644 --- a/apps/modules/lib/ExtensionTestUtils.sys.mjs +++ b/apps/modules/lib/ExtensionTestUtils.sys.mjs @@ -174,10 +174,8 @@ const objectMap = (obj, fn) => */ class ExtensionTestUtilsImpl { /** - * @template {import('resource://app/modules/zora.sys.mjs').IAssert} A - * * @param {Partial} definition - * @param {import('resource://app/modules/ExtensionTestUtils.sys.mjs').AddonMiddleware} assert + * @param {import('resource://app/modules/TestManager.sys.mjs').IDefaultAssert} assert * * @returns {import('resource://app/modules/ExtensionTestUtils.sys.mjs').ExtensionWrapper} */ @@ -189,8 +187,13 @@ class ExtensionTestUtilsImpl { definition.background && serializeScript(definition.background), }) + let testCount = 0 + /** @type {number | null} */ + let expectedTestCount = null + function handleTestResults(kind, pass, msg, ...args) { if (kind == 'test-eq') { + testCount += 1 let [expected, actual] = args assert.ok(pass, `${msg} - Expected: ${expected}, Actual: ${actual}`) } else if (kind == 'test-log') { @@ -228,25 +231,42 @@ class ExtensionTestUtilsImpl { /* Ignore */ } await extension.startup() - return await startupPromise + await startupPromise } catch (e) { assert.fail(`Errored: ${e}`) } + + return this }, async unload() { await extension.shutdown() - return await extension._uninstallPromise + await extension._uninstallPromise + + if (expectedTestCount && testCount !== expectedTestCount) { + assert.fail( + `Expected ${expectedTestCount} to execute. ${testCount} extecuted instead`, + ) + } }, + /** + * @param {number} count + */ + testCount(count) { + expectedTestCount = count + return this + }, sendMsg(msg) { extension.testMessage(msg) + return this }, async awaitMsg(msg) { + const self = this return new Promise((res) => { const callback = (_, event) => { if (event == msg) { extension.off('test-message', callback) - res(void 0) + res(self) } } diff --git a/apps/tests/integrations/_index.sys.mjs b/apps/tests/integrations/_index.sys.mjs index bfda1a6..e61ae6d 100644 --- a/apps/tests/integrations/_index.sys.mjs +++ b/apps/tests/integrations/_index.sys.mjs @@ -4,3 +4,4 @@ // @ts-check /// import './extensions/pageAction.mjs' +import './extensions/tabs.mjs' diff --git a/apps/tests/integrations/extensions/pageAction.mjs b/apps/tests/integrations/extensions/pageAction.mjs index 0828931..3df5f07 100644 --- a/apps/tests/integrations/extensions/pageAction.mjs +++ b/apps/tests/integrations/extensions/pageAction.mjs @@ -17,7 +17,7 @@ async function spinLock(predicate) { } await TestManager.withBrowser('http://example.com/', async (window) => { - await TestManager.test('Extension Test', async (test) => { + await TestManager.test('pageAction', async (test) => { const extension = ExtensionTestUtils.loadExtension( { manifest: { @@ -27,19 +27,6 @@ await TestManager.withBrowser('http://example.com/', async (window) => { show_matches: [''], }, }, - async background() { - const { browser } = this - - browser.test.assertTrue(true, 'True is true') - browser.test.assertEq(1, 1, 'EQ') - browser.test.log('log') - browser.test.sendMessage('msg') - browser.test.succeed('succeed') - - browser.test.onMessage.addListener((msg) => - setTimeout(() => browser.test.sendMessage(`${msg}:done`), 100), - ) - }, files: { 'flask-line.svg': ``, 'pageaction.html': ` @@ -60,9 +47,7 @@ await TestManager.withBrowser('http://example.com/', async (window) => { ) await extension.startup() - - extension.sendMsg('test') - await extension.awaitMsg('test:done') + await new Promise((res) => queueMicrotask(res)) const pageActionId = `page-action-icon--${extension.extension.id}` .replace('@', '') diff --git a/apps/tests/integrations/extensions/tabs.mjs b/apps/tests/integrations/extensions/tabs.mjs new file mode 100644 index 0000000..3d86d98 --- /dev/null +++ b/apps/tests/integrations/extensions/tabs.mjs @@ -0,0 +1,35 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +// @ts-check +/// +import { ExtensionTestUtils } from 'resource://app/modules/ExtensionTestUtils.sys.mjs' +import { TestManager } from 'resource://app/modules/TestManager.sys.mjs' + +await TestManager.withBrowser('http://example.com', async (window) => { + await TestManager.test('tabs', async (test) => { + const extension = ExtensionTestUtils.loadExtension( + { + manifest: { permissions: ['tabs'] }, + async background() { + const { browser } = this + + const exampleTabs = await browser.tabs.query({ + url: 'http://example.com/', + }) + browser.test.assertEq( + exampleTabs.length, + 1, + 'There must be at one tab matching `http://example.com/`', + ) + }, + }, + test, + ) + + await extension + .testCount(1) + .startup() + .then((e) => e.unload()) + }) +}) diff --git a/libs/link/package.json b/libs/link/package.json index 78620b9..46c769e 100644 --- a/libs/link/package.json +++ b/libs/link/package.json @@ -13,6 +13,7 @@ }, "devDependencies": { "gecko-types": "github:quark-platform/gecko-types", + "svelte": "^4.2.8", "zora": "^5.2.0" } } diff --git a/libs/link/types/globals/WindowApi.d.ts b/libs/link/types/globals/WindowApi.d.ts index d90c9f0..c753051 100644 --- a/libs/link/types/globals/WindowApi.d.ts +++ b/libs/link/types/globals/WindowApi.d.ts @@ -13,6 +13,33 @@ declare interface WindowConfiguration { initialUrl: string } +declare type ViewableWritable = { + readOnce(): T +} & import('svelte/store').Writable + +declare interface ITab { + tabId: number | undefined + + title: ViewableWritable + icon: ViewableWritable + uri: ViewableWritable + + hidden: ViewableWritable + + getTabId(): number + getWindowId(): number + getBrowserElement(): XULBrowserElement + + useEventListeners(): void + removeEventListeners(): void + + goBack(): void + goForward(): void + reload(): void + + swapWithTab(tab: ITab): Promise +} + declare type WindowApi = { /** * Identify which window this is. This should be used for actions like tab @@ -34,13 +61,13 @@ declare type WindowApi = { } tabs: { - closeTab(tab: Tab): void - openTab(url?: nsIURIType): Tab - runOnCurrentTab(callback: (tab: Tab) => R): R | undefined - setCurrentTab(tab: Tab): void - getCurrentTab(): Tab | undefined - getTabById(id: number): Tab | undefined - tabs: Tab[] + closeTab(tab: ITab): void + openTab(url?: nsIURIType): ITab + runOnCurrentTab(callback: (tab: ITab) => R): R | undefined + setCurrentTab(tab: ITab): void + getCurrentTab(): ITab | undefined + getTabById(id: number): ITab | undefined + tabs: ITab[] setIcon(browser: XULBrowserElement, iconURL: string): void } diff --git a/libs/link/types/modules/ExtensionTestUtils.d.ts b/libs/link/types/modules/ExtensionTestUtils.d.ts index a4e7cdc..4c577ae 100644 --- a/libs/link/types/modules/ExtensionTestUtils.d.ts +++ b/libs/link/types/modules/ExtensionTestUtils.d.ts @@ -19,11 +19,15 @@ declare module 'resource://app/modules/ExtensionTestUtils.sys.mjs' { export type ExtensionWrapper = { extension: Extension - startup(): Promise<[string, string]> - unload(): Promise - - sendMsg(msg: string): void - awaitMsg(msg: string): Promise + startup(): Promise + unload(): Promise + + /** + * Specifies the number of tests that that this extension should execute + */ + testCount(count: number): ExtensionWrapper + sendMsg(msg: string): ExtensionWrapper + awaitMsg(msg: string): Promise } /** diff --git a/package.json b/package.json index 44d8350..82ed0af 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "prettier": "^3.0.3", "prettier-plugin-organize-imports": "^3.2.3", "prettier-plugin-svelte": "^3.0.3", - "tap-spec": "^5.0.0", + "tap-parser": "^15.3.1", "turbo": "^1.11.2", "typescript": "^5.2.2" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5528085..855b315 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -67,9 +67,9 @@ importers: prettier-plugin-svelte: specifier: ^3.0.3 version: 3.0.3(prettier@3.0.3)(svelte@4.2.8) - tap-spec: - specifier: ^5.0.0 - version: 5.0.0 + tap-parser: + specifier: ^15.3.1 + version: 15.3.1 turbo: specifier: ^1.11.2 version: 1.11.2 @@ -209,6 +209,9 @@ importers: gecko-types: specifier: github:quark-platform/gecko-types version: github.com/quark-platform/gecko-types/4e238b774e4415cbf0ad14b79060f39dd059bd29 + svelte: + specifier: ^4.2.8 + version: 4.2.8(patch_hash=cm43hmf4gczhssi3isoosy53r4) zora: specifier: ^5.2.0 version: 5.2.0 @@ -2850,10 +2853,6 @@ packages: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} dev: true - /buffer-shims@1.0.0: - resolution: {integrity: sha512-Zy8ZXMyxIT6RMTeY7OP/bDndfj6bwCan7SS98CEndS6deHwWPpseeHlwarNcBim+etXnF9HBc1non5JgDaJU1g==} - dev: true - /builtin-modules@3.3.0: resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} engines: {node: '>=6'} @@ -3799,6 +3798,11 @@ packages: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} dev: true + /events-to-array@2.0.3: + resolution: {integrity: sha512-f/qE2gImHRa4Cp2y1stEOSgw8wTFyUdVJX7G//bMwbaV9JqISFxg99NbmVQeP7YLnDUZ2un851jlaDrlpmGehQ==} + engines: {node: '>=12'} + dev: true + /events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -4626,11 +4630,6 @@ packages: engines: {node: '>=0.10.0'} dev: true - /is-finite@1.1.0: - resolution: {integrity: sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==} - engines: {node: '>=0.10.0'} - dev: true - /is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} @@ -5496,11 +5495,6 @@ packages: lines-and-columns: 1.2.4 dev: true - /parse-ms@1.0.1: - resolution: {integrity: sha512-LpH1Cf5EYuVjkBvCDBYvkUPh+iv2bk3FHflxHkpCYT0/FZ1d3N3uJaLiHr4yGuMcFUhv6eAivitTvWZI4B/chg==} - engines: {node: '>=0.10.0'} - dev: true - /parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -5574,11 +5568,6 @@ packages: find-up: 4.1.0 dev: true - /plur@1.0.0: - resolution: {integrity: sha512-qSnKBSZeDY8ApxwhfVIwKwF36KVJqb1/9nzYYq3j3vdwocULCXT8f8fQGkiw1Nk9BGfxiDagEe/pwakA+bOBqw==} - engines: {node: '>=0.10.0'} - dev: true - /postcss-calc@8.2.4(postcss@8.4.31): resolution: {integrity: sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==} peerDependencies: @@ -6029,19 +6018,6 @@ packages: renderkid: 3.0.0 dev: true - /pretty-ms@2.1.0: - resolution: {integrity: sha512-H2enpsxzDhuzRl3zeSQpQMirn8dB0Z/gxW96j06tMfTviUWvX14gjKb7qd1gtkUyYhDPuoNe00K5PqNvy2oQNg==} - engines: {node: '>=0.10.0'} - dependencies: - is-finite: 1.1.0 - parse-ms: 1.0.1 - plur: 1.0.0 - dev: true - - /process-nextick-args@1.0.7: - resolution: {integrity: sha512-yN0WQmuCX63LP/TMvAg31nvT6m4vDqJEiiv2CAZqWOGNWutc9DfDk1NPYYmKUFmaVM2UwDowH4u5AHWYP/jxKw==} - dev: true - /process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} dev: true @@ -6096,22 +6072,6 @@ packages: unpipe: 1.0.0 dev: true - /re-emitter@1.1.3: - resolution: {integrity: sha512-bHJul9CWcocrS+w5e5QrKYXV9NkbSA9hxSEyhYuctwm6keY9NXR2Xt/4A0vbMP0QvuwyfEyb4bkowYXv1ziEbg==} - dev: true - - /readable-stream@2.2.9: - resolution: {integrity: sha512-iuxqX7b7FYt08AriYECxUsK9KTXE3A/FenxIa3IPmvANHxaTP/wGIwwf+IidvvIDk/MsCp/oEV6A8CXo4SDcCg==} - dependencies: - buffer-shims: 1.0.0 - core-util-is: 1.0.3 - inherits: 2.0.4 - isarray: 1.0.0 - process-nextick-args: 1.0.7 - string_decoder: 1.0.3 - util-deprecate: 1.0.2 - dev: true - /readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} dependencies: @@ -6220,11 +6180,6 @@ packages: strip-ansi: 6.0.1 dev: true - /repeat-string@1.6.1: - resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} - engines: {node: '>=0.10'} - dev: true - /require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -6735,12 +6690,6 @@ packages: - supports-color dev: true - /split@1.0.0: - resolution: {integrity: sha512-3SVfJe2A0WZg3D+ZEtXqYkvpSGAVaZ1MgufNCeHioBESCqQFsuT1VcQufiopBfJZqh92ZwQ6ddL378iUSbqVNQ==} - dependencies: - through: 2.3.8 - dev: true - /stable@0.1.8: resolution: {integrity: sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==} deprecated: 'Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility' @@ -6808,12 +6757,6 @@ packages: es-abstract: 1.22.3 dev: true - /string_decoder@1.0.3: - resolution: {integrity: sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==} - dependencies: - safe-buffer: 5.1.2 - dev: true - /string_decoder@1.1.1: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} dependencies: @@ -7033,28 +6976,21 @@ packages: /tabbable@6.2.0: resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} - /tap-out@2.1.0: - resolution: {integrity: sha512-LJE+TBoVbOWhwdz4+FQk40nmbIuxJLqaGvj3WauQw3NYYU5TdjoV3C0x/yq37YAvVyi+oeBXmWnxWSjJ7IEyUw==} + /tap-parser@15.3.1: + resolution: {integrity: sha512-hwAtXX5TBGt2MJeYvASc7DjP48PUzA7P8RTbLxQcgKCEH7ICD5IsRco7l5YvkzjHlZbUbeI9wzO8B4hw2sKgnQ==} + engines: {node: 16 >=16.17.0 || 18 >= 18.6.0 || >=20} hasBin: true dependencies: - re-emitter: 1.1.3 - readable-stream: 2.2.9 - split: 1.0.0 - trim: 0.0.1 + events-to-array: 2.0.3 + tap-yaml: 2.2.1 dev: true - /tap-spec@5.0.0: - resolution: {integrity: sha512-zMDVJiE5I6Y4XGjlueGXJIX2YIkbDN44broZlnypT38Hj/czfOXrszHNNJBF/DXR8n+x6gbfSx68x04kIEHdrw==} - hasBin: true + /tap-yaml@2.2.1: + resolution: {integrity: sha512-ovZuUMLAIH59jnFHXKEGJ+WyDYl6Cuduwg9qpvnqkZOUA1nU84q02Sry1HT0KXcdv2uB91bEKKxnIybBgrb6oA==} + engines: {node: 16 >=16.17.0 || 18 >= 18.6.0 || >=20} dependencies: - chalk: 1.1.3 - duplexer: 0.1.2 - figures: 1.7.0 - lodash: 4.17.21 - pretty-ms: 2.1.0 - repeat-string: 1.6.1 - tap-out: 2.1.0 - through2: 2.0.5 + yaml: 2.3.4 + yaml-types: 0.3.0(yaml@2.3.4) dev: true /tapable@2.2.1: @@ -7101,17 +7037,6 @@ packages: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} dev: true - /through2@2.0.5: - resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} - dependencies: - readable-stream: 2.3.8 - xtend: 4.0.2 - dev: true - - /through@2.3.8: - resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} - dev: true - /thunky@1.1.0: resolution: {integrity: sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==} dev: true @@ -7145,11 +7070,6 @@ packages: hasBin: true dev: true - /trim@0.0.1: - resolution: {integrity: sha512-YzQV+TZg4AxpKxaTHK3c3D+kRDCGVEE7LemdlQZoQXn0iennk10RsIoY6ikzAqJTc9Xjl9C1/waHom/J86ziAQ==} - deprecated: Use String.prototype.trim() instead - dev: true - /truncate-utf8-bytes@1.0.2: resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==} dependencies: @@ -7678,11 +7598,6 @@ packages: optional: true dev: true - /xtend@4.0.2: - resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} - engines: {node: '>=0.4'} - dev: true - /y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -7696,11 +7611,25 @@ packages: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} dev: true + /yaml-types@0.3.0(yaml@2.3.4): + resolution: {integrity: sha512-i9RxAO/LZBiE0NJUy9pbN5jFz5EasYDImzRkj8Y81kkInTi1laia3P3K/wlMKzOxFQutZip8TejvQP/DwgbU7A==} + engines: {node: '>= 16', npm: '>= 7'} + peerDependencies: + yaml: ^2.3.0 + dependencies: + yaml: 2.3.4 + dev: true + /yaml@1.10.2: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} dev: true + /yaml@2.3.4: + resolution: {integrity: sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==} + engines: {node: '>= 14'} + dev: true + /yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} diff --git a/scripts/lib/tapReporter.ts b/scripts/lib/tapReporter.ts new file mode 100644 index 0000000..c0de5b7 --- /dev/null +++ b/scripts/lib/tapReporter.ts @@ -0,0 +1,55 @@ +import kleur from 'kleur' +import { Parser, type Result } from 'tap-parser' + +const { blue, bold, green, grey, red, underline } = kleur + +const SKIP_MARK = blue('⇥') +const SUCCESS_MARK = green('✓') +const FAILURE_MARK = red('✗') +const TODO_MARK = blue('␣') + +const SKIP_COMMENTS = ['tests', 'pass', 'fail', 'skip'].map((c) => '# ' + c) + +export function reporter(watch: boolean) { + const parser = new Parser({ passes: true, flat: false }, (results) => { + console.log('\n\n') + + console.log(`Total:\t${results.count}`) + console.log(green(`Pass:\t${results.pass}`)) + console.log(red(`Fail:\t${results.fail}`)) + console.log(grey(`Skip:\t${results.skip}`)) + console.log(grey(`Todo:\t${results.todo}`)) + + if (!watch) { + process.exit(1) + } + }) + + parser.on('comment', (comment: string) => { + if (SKIP_COMMENTS.some((c) => comment.startsWith(c))) { + return + } + + console.log(`\n\n ${bold(comment)}`) + }) + + parser.on('assert', (res: Result) => { + if (res.skip) { + return console.log(` ${SKIP_MARK} ${grey(res.name)}`) + } + if (res.todo) { + return console.log(` ${TODO_MARK} ${grey(res.name)}`) + } + if (res.ok) { + return console.log(` ${SUCCESS_MARK} ${grey(res.name)}`) + } + + console.log(` ${FAILURE_MARK} ${underline(red(res.name))}`) + for (const key in res.diag) { + const value = res.diag[key] + console.log(grey(` ${key}: ${value}`)) + } + }) + + return parser +} diff --git a/scripts/scripts.d.ts b/scripts/scripts.d.ts index 2fafa51..d2c9be5 100644 --- a/scripts/scripts.d.ts +++ b/scripts/scripts.d.ts @@ -2,7 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -declare module 'tap-spec' { +declare module 'tap-arc' { import { Transform } from 'stream' declare type SpecOptions = { diff --git a/scripts/unit-test.ts b/scripts/unit-test.ts index d4cf59e..bab59e0 100644 --- a/scripts/unit-test.ts +++ b/scripts/unit-test.ts @@ -6,7 +6,8 @@ import { App } from '@tinyhttp/app' import { type ExecaChildProcess, execa } from 'execa' import { createWriteStream } from 'node:fs' import { argv, exit } from 'node:process' -import tapSpec from 'tap-spec' + +import { reporter } from './lib/tapReporter.js' // If you update this port, you should update the port in the test runner const TEST_PORT = 3948 @@ -51,7 +52,7 @@ function createTestReporter( .get('/config', (_, res) => void res.send({ shouldWatch })) .post('/results', (req, res) => { // Provide a nice reporter to the console - req.pipe(tapSpec()).pipe(process.stdout) + req.pipe(reporter(shouldWatch)) if (testProcess) { req