diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a1c84791..9099c99b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,9 +17,6 @@ jobs: with: node-version-file: 'package.json' cache: 'pnpm' - - uses: ueokande/setup-firefox@latest - with: - firefox-version: 78.3.0esr - run: pnpm install --frozen-lockfile - run: pnpm tsc - run: pnpm lint @@ -30,27 +27,21 @@ jobs: name: dist path: ./dist/ - # TODO playwright-webextext does not support MV3 - # test-e2e: - # name: E2E Test - # runs-on: ubuntu-latest - # steps: - # - uses: actions/checkout@v3 - # - uses: actions/setup-node@v3 - # with: - # node-version: '18.5.0' - # cache: 'yarn' - # - uses: ueokande/setup-firefox@latest - # with: - # firefox-version: 78.3.0esr - # - name: Install xsel - # run: sudo apt-get install -y --no-install-recommends xsel - # - run: yarn install --immutable - # - run: yarn build - # - run: $(npm bin)/webext-agent install - # - run: $(npm bin)/webext-agent create-addon --base-addon . /tmp/vimmatic-mixedin - # - name: Run test - # run: | - # export DISPLAY=:99 - # Xvfb -ac :99 -screen 0 1280x1024x24 >/dev/null 2>&1 & - # yarn test:e2e --headed + test-e2e: + name: E2E Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: pnpm/action-setup@v2 + - uses: actions/setup-node@v3 + with: + node-version-file: 'package.json' + cache: 'pnpm' + - name: Install xsel + run: sudo apt-get install -y --no-install-recommends xsel + - run: pnpm install --frozen-lockfile + - run: pnpm exec playwright install firefox + - run: pnpm build + - run: pnpm exec webext-agent install --addon-ids vimmatic@i-beam.org + - run: pnpm exec webext-agent create-addon --base-addon dist/firefox /tmp/vimmatic-mixedin + - run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- pnpm run test:e2e --headed diff --git a/e2e/blacklist.test.ts b/e2e/blacklist.test.ts index 54e931c0..24edd19d 100644 --- a/e2e/blacklist.test.ts +++ b/e2e/blacklist.test.ts @@ -2,16 +2,9 @@ import { test, expect } from "./lib/fixture"; import { newScrollableServer } from "./lib/servers"; import SettingRepository from "./lib/SettingRepository"; -const setupBlacklist = async ( - api: typeof browser, - blacklist: Array }>, -) => { - await new SettingRepository(api).save({ - blacklist, - }); -}; - const server = newScrollableServer(); +const READY_STATE_SELECTOR = + "head[data-vimmatic-content-status='ready'][data-vimmatic-console-status='ready']"; test.beforeAll(async () => { await server.start(); @@ -25,38 +18,25 @@ test("should disable add-on if the URL is in the blacklist", async ({ page, api, }) => { - await setupBlacklist(api, [new URL(server.url()).host + "/a"]); - - await page.goto(server.url("/a")); - await page.keyboard.press("j"); - - const y = await page.evaluate(() => window.pageYOffset); - expect(y).toBe(0); -}); - -test("should enabled add-on if the URL is not in the blacklist", async ({ - page, - api, -}) => { - await setupBlacklist(api, [new URL(server.url()).host + "/a"]); + await new SettingRepository(api).save({ + blacklist: [new URL(server.url()).host + "/a"], + }); - await page.goto(server.url("/ab")); - await page.keyboard.press("j"); + await page.goto(server.url("/a"), { waitUntil: "domcontentloaded" }); + await expect(page.locator(READY_STATE_SELECTOR)).not.toBeAttached(); - const y = await page.evaluate(() => window.pageYOffset); - expect(y).toBe(64); + await page.goto(server.url("/ab"), { waitUntil: "domcontentloaded" }); + await expect(page.locator(READY_STATE_SELECTOR)).toBeAttached(); }); test("should disable keys in the partial blacklist", async ({ page, api }) => { - await setupBlacklist(api, [{ url: new URL(server.url()).host, keys: ["k"] }]); + await new SettingRepository(api).save({ + blacklist: [{ url: new URL(server.url()).host, keys: ["k"] }], + }); await page.goto(server.url()); + await expect(page.locator(READY_STATE_SELECTOR)).toBeAttached(); - await page.keyboard.press("j"); - const y1 = await page.evaluate(() => window.pageYOffset); - expect(y1).toBe(64); - - await page.keyboard.press("k"); - const y2 = await page.evaluate(() => window.pageYOffset); - expect(y2).toBe(64); + await page.keyboard.type("jk"); + expect(await page.evaluate(() => window.scrollY)).toBe(64); }); diff --git a/e2e/clipboard.test.ts b/e2e/clipboard.test.ts index b4f2a543..8deef9f6 100644 --- a/e2e/clipboard.test.ts +++ b/e2e/clipboard.test.ts @@ -1,35 +1,49 @@ import { test, expect } from "./lib/fixture"; import * as clipboard from "./lib/clipboard"; import SettingRepository from "./lib/SettingRepository"; +import { newNopServer } from "./lib/servers"; + +const server = newNopServer(); + +test.beforeAll(async () => { + await server.start(); +}); + +test.afterAll(async () => { + await server.stop(); +}); test("should copy current URL by y", async ({ page }) => { - await page.goto("about:blank#should_copy_url"); + await page.goto(server.url()); await page.keyboard.press("y"); - await expect.poll(() => clipboard.read()).toBe("about:blank#should_copy_url"); + await expect.poll(() => clipboard.read()).toBe(server.url()); }); test("should open an URL from clipboard by p", async ({ page }) => { - await clipboard.write("about:blank#open_from_clipboard"); + await page.goto(server.url()); + await clipboard.write(`${server.url()}#open_to_new_tab`); await page.keyboard.press("p"); - await expect.poll(() => page.url()).toBe("about:blank#open_from_clipboard"); + await expect.poll(() => page.url()).toBe(`${server.url()}#open_to_new_tab`); }); test("should open an URL from clipboard to new tab by P", async ({ page, api, }) => { - const { id: windowId } = await api.windows.getCurrent(); - await clipboard.write("about:blank#open_to_new_tab"); + await page.goto(server.url()); + + await clipboard.write(`${server.url()}#open_to_new_tab`); await page.keyboard.press("Shift+P"); await expect - .poll(() => api.tabs.query({ windowId })) - .toMatchObject([ - { url: "about:blank" }, - { url: "about:blank#open_to_new_tab" }, - ]); + .poll(async () => + (await api.tabs.query({ currentWindow: true })).map((t) => t.url), + ) + .toEqual( + expect.arrayContaining([server.url(), `${server.url()}#open_to_new_tab`]), + ); }); test("should open search result with keywords in clipboard by p", async ({ @@ -38,42 +52,42 @@ test("should open search result with keywords in clipboard by p", async ({ }) => { await new SettingRepository(api).save({ search: { - default: "aboutblank", + default: "localhost", engines: { - aboutblank: "about:blank?q={}", + localhost: `${server.url()}?q={}`, }, }, }); - await page.reload(); + await page.goto(server.url()); await clipboard.write(`an apple`); await page.keyboard.press("p"); - await expect.poll(() => page.url()).toBe("about:blank?q=an%20apple"); + await expect.poll(() => page.url()).toBe(`${server.url()}?q=an%20apple`); }); test("should open search result with keywords in clipboard to new tab by P", async ({ page, api, }) => { - const { id: windowId } = await api.windows.getCurrent(); await new SettingRepository(api).save({ search: { - default: "aboutblank", + default: "localhost", engines: { - aboutblank: "about:blank?q={}", + localhost: `${server.url()}?q={}`, }, }, }); - await page.reload(); + await page.goto(server.url()); await clipboard.write(`an apple`); await page.keyboard.press("Shift+P"); await expect - .poll(() => api.tabs.query({ windowId })) - .toMatchObject([ - { url: "about:blank" }, - { url: "about:blank?q=an%20apple" }, - ]); + .poll(async () => + (await api.tabs.query({ currentWindow: true })).map((t) => t.url), + ) + .toEqual( + expect.arrayContaining([server.url(), `${server.url()}?q=an%20apple`]), + ); }); diff --git a/e2e/colorscheme.test.ts b/e2e/colorscheme.test.ts index f8040975..8bd624b3 100644 --- a/e2e/colorscheme.test.ts +++ b/e2e/colorscheme.test.ts @@ -11,7 +11,7 @@ test.afterAll(async () => { await server.stop(); }); -test("changes color scheme by set command", async ({ page }) => { +test.fixme("changes color scheme by set command", async ({ page }) => { await page.goto(server.url()); await page.console.show(); diff --git a/e2e/command.test.ts b/e2e/command.test.ts index d7cff03e..997fc768 100644 --- a/e2e/command.test.ts +++ b/e2e/command.test.ts @@ -4,6 +4,11 @@ import SettingRepository from "./lib/SettingRepository"; const server = newNopServer(); +// /site1 +// /site2 +// /current +// /site3 +// /site4 const setupTabs = async (api: typeof browser) => { const { id: windowId } = await api.windows.getCurrent(); const tabs = []; @@ -37,93 +42,88 @@ const setupSearchEngines = async (api: typeof browser) => { }); }; -test.describe("addbookmark command", () => { - test("should add a bookmark from the current page", async ({ api, page }) => { - await page.goto(server.url("/bookmark")); - await page.console.exec("addbookmark my great bookmark"); - - await expect - .poll(() => api.bookmarks.search({ title: "my great bookmark" })) - .toMatchObject([{ url: server.url("/bookmark") }]); - }); +test("should add a bookmark by the addbookmark command", async ({ + api, + page, +}) => { + await page.goto(server.url("/bookmark")); + await page.keyboard.type(":addbookmark my great bookmark"); + await page.keyboard.press("Enter"); + + await expect + .poll(() => api.bookmarks.search({ title: "my great bookmark" })) + .toMatchObject([{ url: server.url("/bookmark") }]); }); -test.describe("open command", () => { - test("should open search result for keywords", async ({ page, api }) => { - await setupSearchEngines(api); - await page.reload(); - await page.console.exec("open an apple"); - - await expect - .poll(() => page.url()) - .toBe(server.url("/google") + "?q=an%20apple"); - }); +test("should open a search result by the open command", async ({ + page, + api, +}) => { + await setupSearchEngines(api); + await page.goto(server.url("/")); + await page.keyboard.type(":open an apple"); + await page.keyboard.press("Enter"); + + await expect + .poll(() => page.url()) + .toBe(server.url("/google") + "?q=an%20apple"); }); -test.describe("buffer command", () => { - test("should select a tab by title", async ({ page, api }) => { - const { id: windowId } = await api.windows.getCurrent(); - await setupTabs(api); - - await page.console.exec("buffer site1"); - - await expect - .poll(() => api.tabs.query({ windowId })) - .toMatchObject([ - { url: server.url("/site1"), active: true }, - { url: server.url("/site2") }, - { url: "about:blank" }, - { url: server.url("/site3") }, - { url: server.url("/site4") }, - ]); - }); +test("should select a tab by the buffer command", async ({ page, api }) => { + const { id: windowId } = await api.windows.getCurrent(); + await setupTabs(api); + await page.goto(server.url("/current")); + + await page.keyboard.type(":buffer site1"); + await page.keyboard.press("Enter"); + + await expect + .poll(() => api.tabs.query({ windowId })) + .toMatchObject([ + { url: server.url("/site1"), active: true }, + { url: server.url("/site2") }, + { url: server.url("/current") }, + { url: server.url("/site3") }, + { url: server.url("/site4") }, + ]); }); -test.describe("bdelete command", () => { - test("should delete a tab by keyword", async ({ page, api }) => { - const { id: windowId } = await api.windows.getCurrent(); - await setupTabs(api); - - await page.console.exec("bdelete site3"); - await expect - .poll(() => api.tabs.query({ windowId })) - .toMatchObject([ - { url: server.url("/site1") }, - { url: server.url("/site2") }, - { url: "about:blank" }, - { url: server.url("/site4") }, - ]); - - await page.console.exec("bdelete! site2"); - await expect - .poll(() => api.tabs.query({ windowId })) - .toMatchObject([ - { url: server.url("/site1") }, - { url: "about:blank" }, - { url: server.url("/site4") }, - ]); - }); +test("should close a tab by the bdelete command", async ({ page, api }) => { + const { id: windowId } = await api.windows.getCurrent(); + await setupTabs(api); + await page.goto(server.url("/current")); + + await page.keyboard.type(":bdelete site3"); + await page.keyboard.press("Enter"); + await expect + .poll(() => api.tabs.query({ windowId })) + .toMatchObject([ + { url: server.url("/site1") }, + { url: server.url("/site2") }, + { url: server.url("/current") }, + { url: server.url("/site4") }, + ]); }); -test.describe("bdeletes command", () => { - test("should delete tabs by keyword", async ({ page, api }) => { - const { id: windowId } = await api.windows.getCurrent(); - await setupTabs(api); - - await page.console.exec("bdeletes site"); - - await expect - .poll(() => api.tabs.query({ windowId })) - .toMatchObject([ - { url: server.url("/site1") }, - { url: server.url("/site2") }, - { url: "about:blank" }, - ]); - - await page.console.exec("bdeletes! site"); - - await expect - .poll(() => api.tabs.query({ windowId })) - .toMatchObject([{ url: "about:blank" }]); - }); +test("should close tabs by the bdeletes command", async ({ page, api }) => { + const { id: windowId } = await api.windows.getCurrent(); + await setupTabs(api); + await page.goto(server.url("/current")); + + await page.keyboard.type(":bdeletes site"); + await page.keyboard.press("Enter"); + + await expect + .poll(() => api.tabs.query({ windowId })) + .toMatchObject([ + { url: server.url("/site1") }, + { url: server.url("/site2") }, + { url: server.url("/current") }, + ]); + + await page.keyboard.type(":bdeletes! site"); + await page.keyboard.press("Enter"); + await expect + .poll(() => api.tabs.query({ windowId })) + .toMatchObject([{ url: server.url("/current") }]); }); diff --git a/e2e/completion.test.ts b/e2e/completion.test.ts index e5fdc08d..30657826 100644 --- a/e2e/completion.test.ts +++ b/e2e/completion.test.ts @@ -1,6 +1,6 @@ import { test, expect } from "./lib/fixture"; -test("should shows all commands on empty line", async ({ page }) => { +test.fixme("should shows all commands on empty line", async ({ page }) => { await page.console.show(); await expect @@ -8,7 +8,7 @@ test("should shows all commands on empty line", async ({ page }) => { .toMatchObject([{ title: "Console Command", items: { length: 11 } }]); }); -test("should shows commands filtered by prefix", async ({ page }) => { +test.fixme("should shows commands filtered by prefix", async ({ page }) => { await page.console.show(); await page.console.type("b"); @@ -26,82 +26,85 @@ test("should shows commands filtered by prefix", async ({ page }) => { // > bdelete // > bdeletes // : b -test("selects completion items by / keys", async ({ page }) => { - await page.console.show(); - await page.console.type("b"); +test.fixme( + "selects completion items by / keys", + async ({ page }) => { + await page.console.show(); + await page.console.type("b"); - await expect - .poll(() => page.console.getCompletion()) - .toMatchObject([ - { - title: "Console Command", - items: [ - { text: "buffer", highlight: false }, - { text: "bdelete", highlight: false }, - { text: "bdeletes", highlight: false }, - ], - }, - ]); - await expect.poll(() => page.console.getCommand()).toBe("b"); + await expect + .poll(() => page.console.getCompletion()) + .toMatchObject([ + { + title: "Console Command", + items: [ + { text: "buffer", highlight: false }, + { text: "bdelete", highlight: false }, + { text: "bdeletes", highlight: false }, + ], + }, + ]); + await expect.poll(() => page.console.getCommand()).toBe("b"); - await page.keyboard.press("Tab"); - await expect - .poll(() => page.console.getCompletion()) - .toMatchObject([ - { - title: "Console Command", - items: [ - { text: "buffer", highlight: true }, - { text: "bdelete", highlight: false }, - { text: "bdeletes", highlight: false }, - ], - }, - ]); - await expect.poll(() => page.console.getCommand()).toBe("buffer"); + await page.keyboard.press("Tab"); + await expect + .poll(() => page.console.getCompletion()) + .toMatchObject([ + { + title: "Console Command", + items: [ + { text: "buffer", highlight: true }, + { text: "bdelete", highlight: false }, + { text: "bdeletes", highlight: false }, + ], + }, + ]); + await expect.poll(() => page.console.getCommand()).toBe("buffer"); - await page.keyboard.press("Tab"); - await page.keyboard.press("Tab"); - await expect - .poll(() => page.console.getCompletion()) - .toMatchObject([ - { - title: "Console Command", - items: [ - { text: "buffer", highlight: false }, - { text: "bdelete", highlight: false }, - { text: "bdeletes", highlight: true }, - ], - }, - ]); - await expect.poll(() => page.console.getCommand()).toBe("bdeletes"); + await page.keyboard.press("Tab"); + await page.keyboard.press("Tab"); + await expect + .poll(() => page.console.getCompletion()) + .toMatchObject([ + { + title: "Console Command", + items: [ + { text: "buffer", highlight: false }, + { text: "bdelete", highlight: false }, + { text: "bdeletes", highlight: true }, + ], + }, + ]); + await expect.poll(() => page.console.getCommand()).toBe("bdeletes"); - await page.keyboard.press("Tab"); - await expect - .poll(() => page.console.getCompletion()) - .toMatchObject([ - { - title: "Console Command", - items: [ - { text: "buffer", highlight: false }, - { text: "bdelete", highlight: false }, - { text: "bdeletes", highlight: false }, - ], - }, - ]); - await expect.poll(() => page.console.getCommand()).toBe("b"); + await page.keyboard.press("Tab"); + await expect + .poll(() => page.console.getCompletion()) + .toMatchObject([ + { + title: "Console Command", + items: [ + { text: "buffer", highlight: false }, + { text: "bdelete", highlight: false }, + { text: "bdeletes", highlight: false }, + ], + }, + ]); + await expect.poll(() => page.console.getCommand()).toBe("b"); - await page.keyboard.press("Shift+Tab"); - await expect - .poll(() => page.console.getCompletion()) - .toMatchObject([ - { - title: "Console Command", - items: [ - { text: "buffer", highlight: false }, - { text: "bdelete", highlight: false }, - { text: "bdeletes", highlight: true }, - ], - }, - ]); - await expect.poll(() => page.console.getCommand()).toBe("bdeletes"); -}); + await page.keyboard.press("Shift+Tab"); + await expect + .poll(() => page.console.getCompletion()) + .toMatchObject([ + { + title: "Console Command", + items: [ + { text: "buffer", highlight: false }, + { text: "bdelete", highlight: false }, + { text: "bdeletes", highlight: true }, + ], + }, + ]); + await expect.poll(() => page.console.getCommand()).toBe("bdeletes"); + }, +); diff --git a/e2e/completion_buffers.test.ts b/e2e/completion_buffers.test.ts index a5f7d3a8..5e49a204 100644 --- a/e2e/completion_buffers.test.ts +++ b/e2e/completion_buffers.test.ts @@ -23,169 +23,169 @@ test.afterAll(async () => { await server.stop(); }); -test('should all tabs by "buffer" command with empty params', async ({ - page, - api, -}) => { - await setupTabs(api); - await page.keyboard.press("Shift+J"); - await page.keyboard.press("Shift+K"); - await page.console.show(); - await page.console.type("buffer "); - - await expect - .poll(() => page.console.getCompletion()) - .toMatchObject([ - { - title: "Buffers", - items: [ - { text: "1: Site site_a" }, - { text: "2: Site site_b" }, - { text: "3: % New Tab" }, - { text: "4: # Site site_c" }, - { text: "5: Site site_d" }, - ], - }, - ]); -}); - -test('should filter items with URLs by keywords on "buffer" command', async ({ - page, - api, -}) => { - await setupTabs(api); - await page.console.show(); - await page.console.type("buffer /site_b"); - - await expect - .poll(() => page.console.getCompletion()) - .toMatchObject([ - { - title: "Buffers", - items: [{ text: "2: Site site_b" }], - }, - ]); -}); - -test('should filter items with titles by keywords on "buffer" command', async ({ - page, - api, -}) => { - await setupTabs(api); - await page.console.show(); - await page.console.type("buffer Site"); - - await expect - .poll(() => page.console.getCompletion()) - .toMatchObject([ - { - title: "Buffers", - items: [ - { text: "1: Site site_a" }, - { text: "2: Site site_b" }, - { text: "4: Site site_c" }, - { text: "5: Site site_d" }, - ], - }, - ]); -}); - -test('should show one item by number on "buffer" command', async ({ - page, - api, -}) => { - await setupTabs(api); - await page.console.show(); - await page.console.type("buffer 2"); - - await expect - .poll(() => page.console.getCompletion()) - .toMatchObject([ - { - title: "Buffers", - items: [{ text: "2: Site site_b" }], - }, - ]); -}); - -test('should show only unpinned tabs "bdelete" command', async ({ - page, - api, -}) => { - await setupTabs(api); - await page.console.show(); - await page.console.type("bdelete Site"); - - await expect - .poll(() => page.console.getCompletion()) - .toMatchObject([ - { - title: "Buffers", - items: [{ text: "4: Site site_c" }, { text: "5: Site site_d" }], - }, - ]); -}); - -test('should show only unpinned tabs "bdeletes" command', async ({ - page, - api, -}) => { - await setupTabs(api); - await page.console.show(); - await page.console.type("bdeletes Site"); - - await expect - .poll(() => page.console.getCompletion()) - .toMatchObject([ - { - title: "Buffers", - items: [{ text: "4: Site site_c" }, { text: "5: Site site_d" }], - }, - ]); -}); - -test('should show both pinned and unpinned tabs "bdelete!" command', async ({ - page, - api, -}) => { - await setupTabs(api); - await page.console.show(); - await page.console.type("bdelete! Site"); - - await expect - .poll(() => page.console.getCompletion()) - .toMatchObject([ - { - title: "Buffers", - items: [ - { text: "1: Site site_a" }, - { text: "2: Site site_b" }, - { text: "4: Site site_c" }, - { text: "5: Site site_d" }, - ], - }, - ]); -}); - -test('should show both pinned and unpinned tabs "bdeletes!" command', async ({ - page, - api, -}) => { - await setupTabs(api); - await page.console.show(); - await page.console.type("bdelete! Site"); - - await expect - .poll(() => page.console.getCompletion()) - .toMatchObject([ - { - title: "Buffers", - items: [ - { text: "1: Site site_a" }, - { text: "2: Site site_b" }, - { text: "4: Site site_c" }, - { text: "5: Site site_d" }, - ], - }, - ]); -}); +test.fixme( + 'should all tabs by "buffer" command with empty params', + async ({ page, api }) => { + await setupTabs(api); + await page.keyboard.press("Shift+J"); + await page.keyboard.press("Shift+K"); + await page.console.show(); + await page.console.type("buffer "); + + await expect + .poll(() => page.console.getCompletion()) + .toMatchObject([ + { + title: "Buffers", + items: [ + { text: "1: Site site_a" }, + { text: "2: Site site_b" }, + { text: "3: % New Tab" }, + { text: "4: # Site site_c" }, + { text: "5: Site site_d" }, + ], + }, + ]); + }, +); + +test.fixme( + 'should filter items with URLs by keywords on "buffer" command', + async ({ page, api }) => { + await setupTabs(api); + await page.console.show(); + await page.console.type("buffer /site_b"); + + await expect + .poll(() => page.console.getCompletion()) + .toMatchObject([ + { + title: "Buffers", + items: [{ text: "2: Site site_b" }], + }, + ]); + }, +); + +test.fixme( + 'should filter items with titles by keywords on "buffer" command', + async ({ page, api }) => { + await setupTabs(api); + await page.console.show(); + await page.console.type("buffer Site"); + + await expect + .poll(() => page.console.getCompletion()) + .toMatchObject([ + { + title: "Buffers", + items: [ + { text: "1: Site site_a" }, + { text: "2: Site site_b" }, + { text: "4: Site site_c" }, + { text: "5: Site site_d" }, + ], + }, + ]); + }, +); + +test.fixme( + 'should show one item by number on "buffer" command', + async ({ page, api }) => { + await setupTabs(api); + await page.console.show(); + await page.console.type("buffer 2"); + + await expect + .poll(() => page.console.getCompletion()) + .toMatchObject([ + { + title: "Buffers", + items: [{ text: "2: Site site_b" }], + }, + ]); + }, +); + +test.fixme( + 'should show only unpinned tabs "bdelete" command', + async ({ page, api }) => { + await setupTabs(api); + await page.console.show(); + await page.console.type("bdelete Site"); + + await expect + .poll(() => page.console.getCompletion()) + .toMatchObject([ + { + title: "Buffers", + items: [{ text: "4: Site site_c" }, { text: "5: Site site_d" }], + }, + ]); + }, +); + +test.fixme( + 'should show only unpinned tabs "bdeletes" command', + async ({ page, api }) => { + await setupTabs(api); + await page.console.show(); + await page.console.type("bdeletes Site"); + + await expect + .poll(() => page.console.getCompletion()) + .toMatchObject([ + { + title: "Buffers", + items: [{ text: "4: Site site_c" }, { text: "5: Site site_d" }], + }, + ]); + }, +); + +test.fixme( + 'should show both pinned and unpinned tabs "bdelete!" command', + async ({ page, api }) => { + await setupTabs(api); + await page.console.show(); + await page.console.type("bdelete! Site"); + + await expect + .poll(() => page.console.getCompletion()) + .toMatchObject([ + { + title: "Buffers", + items: [ + { text: "1: Site site_a" }, + { text: "2: Site site_b" }, + { text: "4: Site site_c" }, + { text: "5: Site site_d" }, + ], + }, + ]); + }, +); + +test.fixme( + 'should show both pinned and unpinned tabs "bdeletes!" command', + async ({ page, api }) => { + await setupTabs(api); + await page.console.show(); + await page.console.type("bdelete! Site"); + + await expect + .poll(() => page.console.getCompletion()) + .toMatchObject([ + { + title: "Buffers", + items: [ + { text: "1: Site site_a" }, + { text: "2: Site site_b" }, + { text: "4: Site site_c" }, + { text: "5: Site site_d" }, + ], + }, + ]); + }, +); diff --git a/e2e/completion_open.test.ts b/e2e/completion_open.test.ts index 60536609..f6fb069f 100644 --- a/e2e/completion_open.test.ts +++ b/e2e/completion_open.test.ts @@ -12,98 +12,102 @@ test.afterAll(async () => { await server.stop(); }); -test('should show completions from search engines, bookmarks, and histories by "open" command', async ({ - page, - api, -}) => { - await api.history.addUrl({ - url: "https://example.com/", - title: "Example Domain", - }); - await page.console.show(); - await page.console.type("open "); - - await expect - .poll(() => page.console.getCompletion()) - .toMatchObject([ - { title: "Search Engines" }, - { title: "Bookmarks" }, - { title: "History" }, - ]); -}); - -test('should filter items with titles by keywords on "open" command', async ({ - page, -}) => { - await page.console.show(); - await page.console.type("open getting"); - - await expect - .poll(() => page.console.getCompletion()) - .toMatchObject([ - { title: "Bookmarks", items: [{ text: "Getting Started" }] }, - ]); -}); - -test('should filter items with titles by keywords on "tabopen" command', async ({ - page, -}) => { - await page.console.show(); - await page.console.type("tabopen getting"); - - await expect - .poll(() => page.console.getCompletion()) - .toMatchObject([ - { title: "Bookmarks", items: [{ text: "Getting Started" }] }, - ]); -}); - -test('should filter items with titles by keywords on "winopen" command', async ({ - page, -}) => { - await page.console.show(); - await page.console.type("winopen getting"); - - await expect - .poll(() => page.console.getCompletion()) - .toMatchObject([ - { title: "Bookmarks", items: [{ text: "Getting Started" }] }, - ]); -}); - -test('should display only specified items in "complete" property by set command', async ({ - page, -}) => { - await page.console.exec("set complete=bss"); - await page.console.show(); - await page.console.type("open "); - - await expect - .poll(() => page.console.getCompletion()) - .toMatchObject([ - { title: "Bookmarks" }, - { title: "Search Engines" }, - { title: "Search Engines" }, - ]); -}); - -test('should display only specified items in "complete" property by setting', async ({ - page, - api, -}) => { - await new SettingRepository(api).save({ - properties: { complete: "bss" }, - }); - - await page.reload(); - await page.console.show(); - await page.console.type("open "); - - await expect - .poll(() => page.console.getCompletion()) - .toMatchObject([ - { title: "Bookmarks" }, - { title: "Search Engines" }, - { title: "Search Engines" }, - ]); -}); +test.fixme( + 'should show completions from search engines, bookmarks, and histories by "open" command', + async ({ page, api }) => { + await api.history.addUrl({ + url: "https://example.com/", + title: "Example Domain", + }); + await page.console.show(); + await page.console.type("open "); + + await expect + .poll(() => page.console.getCompletion()) + .toMatchObject([ + { title: "Search Engines" }, + { title: "Bookmarks" }, + { title: "History" }, + ]); + }, +); + +test.fixme( + 'should filter items with titles by keywords on "open" command', + async ({ page }) => { + await page.console.show(); + await page.console.type("open getting"); + + await expect + .poll(() => page.console.getCompletion()) + .toMatchObject([ + { title: "Bookmarks", items: [{ text: "Getting Started" }] }, + ]); + }, +); + +test.fixme( + 'should filter items with titles by keywords on "tabopen" command', + async ({ page }) => { + await page.console.show(); + await page.console.type("tabopen getting"); + + await expect + .poll(() => page.console.getCompletion()) + .toMatchObject([ + { title: "Bookmarks", items: [{ text: "Getting Started" }] }, + ]); + }, +); + +test.fixme( + 'should filter items with titles by keywords on "winopen" command', + async ({ page }) => { + await page.console.show(); + await page.console.type("winopen getting"); + + await expect + .poll(() => page.console.getCompletion()) + .toMatchObject([ + { title: "Bookmarks", items: [{ text: "Getting Started" }] }, + ]); + }, +); + +test.fixme( + 'should display only specified items in "complete" property by set command', + async ({ page }) => { + await page.console.exec("set complete=bss"); + await page.console.show(); + await page.console.type("open "); + + await expect + .poll(() => page.console.getCompletion()) + .toMatchObject([ + { title: "Bookmarks" }, + { title: "Search Engines" }, + { title: "Search Engines" }, + ]); + }, +); + +test.fixme( + 'should display only specified items in "complete" property by setting', + async ({ page, api }) => { + await new SettingRepository(api).save({ + properties: { complete: "bss" }, + }); + + await page.reload(); + await page.console.show(); + await page.console.type("open "); + + await expect + .poll(() => page.console.getCompletion()) + .toMatchObject([ + { title: "Bookmarks" }, + { title: "Search Engines" }, + { title: "Search Engines" }, + ]); + }, +); diff --git a/e2e/completion_set.test.ts b/e2e/completion_set.test.ts index 32a085d1..326ace5f 100644 --- a/e2e/completion_set.test.ts +++ b/e2e/completion_set.test.ts @@ -1,31 +1,32 @@ import { test, expect } from "./lib/fixture"; -test('should show all property names by "set" command with empty params', async ({ - page, -}) => { - await page.console.show(); - await page.console.type("set "); +test.fixme( + 'should show all property names by "set" command with empty params', + async ({ page }) => { + await page.console.show(); + await page.console.type("set "); - await expect - .poll(() => page.console.getCompletion()) - .toMatchObject([ - { - title: "Properties", - items: [ - { text: "hintchars" }, - { text: "smoothscroll" }, - { text: "nosmoothscroll" }, - { text: "complete" }, - { text: "colorscheme" }, - ], - }, - ]); + await expect + .poll(() => page.console.getCompletion()) + .toMatchObject([ + { + title: "Properties", + items: [ + { text: "hintchars" }, + { text: "smoothscroll" }, + { text: "nosmoothscroll" }, + { text: "complete" }, + { text: "colorscheme" }, + ], + }, + ]); - await page.console.type("no"); + await page.console.type("no"); - await expect - .poll(() => page.console.getCompletion()) - .toMatchObject([ - { title: "Properties", items: [{ text: "nosmoothscroll" }] }, - ]); -}); + await expect + .poll(() => page.console.getCompletion()) + .toMatchObject([ + { title: "Properties", items: [{ text: "nosmoothscroll" }] }, + ]); + }, +); diff --git a/e2e/console.test.ts b/e2e/console.test.ts index 58e4ef1a..f10069af 100644 --- a/e2e/console.test.ts +++ b/e2e/console.test.ts @@ -1,69 +1,73 @@ import { test, expect } from "./lib/fixture"; -test("open console with :", async ({ page }) => { +test.fixme("open console with :", async ({ page }) => { await page.console.show(); await expect.poll(() => page.console.getCommand()).toBe(""); }); -test("open console with open command by o", async ({ page }) => { +test.fixme("open console with open command by o", async ({ page }) => { await page.keyboard.press("o"); await expect.poll(() => page.console.getCommand()).toBe("open "); }); -test("open console with open command and current URL by O", async ({ - page, -}) => { - await page.keyboard.press("Shift+O"); +test.fixme( + "open console with open command and current URL by O", + async ({ page }) => { + await page.keyboard.press("Shift+O"); - await expect.poll(() => page.console.getCommand()).toBe("open about:blank"); -}); + await expect.poll(() => page.console.getCommand()).toBe("open about:blank"); + }, +); -test("open console with tabopen command by t", async ({ page }) => { +test.fixme("open console with tabopen command by t", async ({ page }) => { await page.keyboard.press("t"); await expect.poll(() => page.console.getCommand()).toBe("tabopen "); }); -test("open console with tabopen command and current URL by T", async ({ - page, -}) => { - await page.keyboard.press("Shift+T"); +test.fixme( + "open console with tabopen command and current URL by T", + async ({ page }) => { + await page.keyboard.press("Shift+T"); - await expect - .poll(() => page.console.getCommand()) - .toBe("tabopen about:blank"); -}); + await expect + .poll(() => page.console.getCommand()) + .toBe("tabopen about:blank"); + }, +); -test("open console with winopen command by w", async ({ page }) => { +test.fixme("open console with winopen command by w", async ({ page }) => { await page.keyboard.press("w"); await expect.poll(() => page.console.getCommand()).toBe("winopen "); }); -test("open console with winopen command and current URL by W", async ({ - page, -}) => { - await page.keyboard.press("Shift+W"); +test.fixme( + "open console with winopen command and current URL by W", + async ({ page }) => { + await page.keyboard.press("Shift+W"); - await expect - .poll(() => page.console.getCommand()) - .toBe("winopen about:blank"); -}); + await expect + .poll(() => page.console.getCommand()) + .toBe("winopen about:blank"); + }, +); -test("open console with buffer command by b", async ({ page }) => { +test.fixme("open console with buffer command by b", async ({ page }) => { await page.keyboard.press("b"); await expect.poll(() => page.console.getCommand()).toBe("buffer "); }); -test("open console with addbookmark command with title by a", async ({ - page, -}) => { - await page.keyboard.press("a"); +test.fixme( + "open console with addbookmark command with title by a", + async ({ page }) => { + await page.keyboard.press("a"); - await expect - .poll(() => page.console.getCommand()) - .toBe("addbookmark New Tab"); -}); + await expect + .poll(() => page.console.getCommand()) + .toBe("addbookmark New Tab"); + }, +); diff --git a/e2e/find.test.ts b/e2e/find.test.ts index 8f68d328..33c650b5 100644 --- a/e2e/find.test.ts +++ b/e2e/find.test.ts @@ -1,3 +1,4 @@ +import { type Page } from "@playwright/test"; import { test, expect } from "./lib/fixture"; import { newSingleContentServer } from "./lib/servers"; @@ -5,6 +6,14 @@ const server = newSingleContentServer( `--hello--hello--hello--`, ); +const expectSelection = (page: Page) => + expect.poll(() => + page.evaluate(() => { + const selection = window.getSelection(); + return { from: selection.anchorOffset, to: selection.focusOffset }; + }), + ); + test.beforeAll(async () => { await server.start(); }); @@ -15,58 +24,42 @@ test.afterAll(async () => { test("starts searching", async ({ page }) => { await page.goto(server.url()); - await page.console.find("hello"); - await expect - .poll(() => page.selection.getRange()) - .toEqual({ from: 2, to: 7 }); + await page.keyboard.type("/hello"); + await page.keyboard.press("Enter"); + await expectSelection(page).toEqual({ from: 2, to: 7 }); // search next keyword await page.keyboard.press("n"); - await expect - .poll(() => page.selection.getRange()) - .toEqual({ from: 9, to: 14 }); + await expectSelection(page).toEqual({ from: 9, to: 14 }); // search previous keyword await page.keyboard.press("Shift+N"); - await expect - .poll(() => page.selection.getRange()) - .toEqual({ from: 2, to: 7 }); + await expectSelection(page).toEqual({ from: 2, to: 7 }); // search previous keyword by wrap-search await page.keyboard.press("Shift+N"); - await expect - .poll(() => page.selection.getRange()) - .toEqual({ from: 16, to: 21 }); + await expectSelection(page).toEqual({ from: 16, to: 21 }); }); -test("shows error if pattern not found", async ({ page }) => { +test("search with last keyword if keyword is empty", async ({ page }) => { await page.goto(server.url()); - await page.console.find("world"); - await expect - .poll(() => page.console.getErrorMessage()) - .toBe("Pattern not found: world"); -}); + await page.keyboard.type("/hello"); + await page.keyboard.press("Enter"); -test("search with last keyword if keyword is empty", async ({ page }) => { - await page.goto(server.url()); - await page.console.find("hello"); - await page.console.find(""); + await page.keyboard.type("/"); + await page.keyboard.press("Enter"); - await expect - .poll(() => page.selection.getRange()) - .toEqual({ from: 2, to: 7 }); + await expectSelection(page).toEqual({ from: 2, to: 7 }); }); test("search with last keyword on new page", async ({ page }) => { await page.goto(server.url()); - await page.console.find("hello"); + await page.keyboard.type("/hello"); + await page.keyboard.press("Enter"); await page.reload(); await page.keyboard.press("n"); - - await expect - .poll(() => page.selection.getRange()) - .toEqual({ from: 2, to: 7 }); + await expectSelection(page).toEqual({ from: 2, to: 7 }); }); diff --git a/e2e/follow.test.ts b/e2e/follow.test.ts index 62777674..99024a47 100644 --- a/e2e/follow.test.ts +++ b/e2e/follow.test.ts @@ -119,8 +119,9 @@ test("should focus an input by f", async ({ page }) => { await page.locator("text=a").waitFor(); await page.keyboard.press("a"); - const activeTag = await page.evaluate(() => document.activeElement.tagName); - expect(activeTag.toLowerCase()).toBe("input"); + await expect + .poll(() => page.evaluate(() => document.activeElement.tagName)) + .toBe("INPUT"); }); test("should open a link by f", async ({ page, api }) => { @@ -130,7 +131,6 @@ test("should open a link by f", async ({ page, api }) => { await page.keyboard.press("f"); await page.locator("text=a").waitFor(); await page.keyboard.press("a"); - await page.waitForNavigation(); await expect .poll(() => api.tabs.query({ windowId })) @@ -155,7 +155,7 @@ test("should show hints of links in area", async ({ page }) => { await page.keyboard.press("f"); await page.locator("text=a").waitFor(); - const hints = await page.locator(".vimmatic-hint").allInnerTexts(); + const hints = await page.locator("[data-vimmatic-hint]").allInnerTexts(); expect(hints.length).toBe(3); }); @@ -164,7 +164,7 @@ test("should shows hints only in viewport", async ({ page }) => { await page.keyboard.press("f"); await page.locator("text=a").waitFor(); - const hints = await page.locator(".vimmatic-hint").allInnerTexts(); + const hints = await page.locator("[data-vimmatic-hint]").allInnerTexts(); expect(hints.length).toBe(1); }); @@ -175,7 +175,7 @@ test("should shows hints only in window of the frame", async ({ page }) => { const hints = await page .frame("inner-frame") - .locator(".vimmatic-hint") + .locator("[data-vimmatic-hint]") .allInnerTexts(); expect(hints.length).toBe(1); }); @@ -187,7 +187,7 @@ test("should shows hints only in the frame", async ({ page }) => { const hints = await page .frame("inner-frame") - .locator(".vimmatic-hint") + .locator("[data-vimmatic-hint]") .allInnerTexts(); expect(hints.length).toBe(1); }); diff --git a/e2e/follow_properties.test.ts b/e2e/follow_properties.test.ts index 50b26c5b..74369b4b 100644 --- a/e2e/follow_properties.test.ts +++ b/e2e/follow_properties.test.ts @@ -47,15 +47,15 @@ test("should show hints with hintchars by settings", async ({ page, api }) => { await setupHintchars(api); await page.goto(server.url()); await page.keyboard.press("f"); - await page.locator(".vimmatic-hint").first().waitFor(); - let hints = await page.locator(".vimmatic-hint:visible").allInnerTexts(); - expect(hints).toEqual(["J", "K", "JJ", "JK", "KJ"]); + await expect + .poll(() => page.locator("[data-vimmatic-hint]:visible").allInnerTexts()) + .toEqual(["J", "K", "JJ", "JK", "KJ"]); await page.keyboard.press("j"); - - hints = await page.locator(".vimmatic-hint:visible").allInnerTexts(); - expect(hints).toEqual(["J", "JJ", "JK"]); + await expect + .poll(() => page.locator("[data-vimmatic-hint]:visible").allInnerTexts()) + .toEqual(["J", "JJ", "JK"]); }); test("should open link into a new tab", async ({ page, api }) => { @@ -64,7 +64,8 @@ test("should open link into a new tab", async ({ page, api }) => { await setupHintchars(api); await page.goto(server.url()); await page.keyboard.press("Shift+F"); - await page.locator(".vimmatic-hint").first().waitFor(); + await page.locator("[data-vimmatic-hint]:visible").first().waitFor(); + await new Promise((resolve) => setTimeout(resolve, 1000)); await page.keyboard.press("j"); await page.keyboard.press("j"); @@ -79,7 +80,7 @@ test("should open link into new tab in background", async ({ page, api }) => { await setupHintchars(api); await page.goto(server.url()); await page.keyboard.press("Control+f"); - await page.locator(".vimmatic-hint").first().waitFor(); + await page.locator("[data-vimmatic-hint]:visible").first().waitFor(); await page.keyboard.press("j"); await page.keyboard.press("j"); @@ -90,12 +91,12 @@ test("should open link into new tab in background", async ({ page, api }) => { test("should show hints with set hintchars", async ({ page }) => { await page.goto(server.url()); - await page.console.exec("set hintchars=abc"); - await new Promise((resolve) => setTimeout(resolve, 500)); + await page.keyboard.type(":set hintchars=abc"); + await page.keyboard.press("Enter"); await page.keyboard.press("f"); - await page.locator(".vimmatic-hint").first().waitFor(); - const hints = await page.locator(".vimmatic-hint:visible").allInnerTexts(); - expect(hints).toEqual(["A", "B", "C", "AA", "AB"]); + await expect + .poll(() => page.locator("[data-vimmatic-hint]:visible").allInnerTexts()) + .toEqual(["A", "B", "C", "AA", "AB"]); }); diff --git a/e2e/lib/Console.ts b/e2e/lib/Console.ts deleted file mode 100644 index 064d0bd1..00000000 --- a/e2e/lib/Console.ts +++ /dev/null @@ -1,94 +0,0 @@ -import type { Page } from "@playwright/test"; - -type CompletionGroup = { - title: string; - items: Array<{ text: string; highlight: boolean }>; -}; - -export default class Console { - private readonly page: Page; - - constructor(page: Page) { - this.page = page; - } - - async show() { - await this.page - .locator("#vimmatic-console-frame") - .waitFor({ state: "attached" }); - await this.page.keyboard.press(":"); - await this.consoleFrame() - .locator("[role=menu]") - .waitFor({ state: "visible" }); - } - - async exec(command: string) { - await this.show(); - await this.type(command); - await this.enter(); - } - - async find(keyword: string) { - await this.page - .locator("#vimmatic-console-frame") - .waitFor({ state: "attached" }); - await this.page.keyboard.press("/"); - await this.type(keyword); - await this.enter(); - } - - async type(str: string) { - await this.consoleFrame().locator("input").waitFor({ state: "visible" }); - await this.page.keyboard.type(str); - } - - async enter() { - await this.page.keyboard.press("Enter"); - } - - async getCommand(): Promise { - return this.consoleFrame().evaluate(() => { - return document.querySelector("input").value; - }); - } - - async getCompletion(): Promise { - return this.consoleFrame().evaluate(() => { - const groups = document.querySelectorAll("[role=group]"); - if (groups.length === 0) { - throw new Error("completion items not found"); - } - - return Array.from(groups).map((group) => { - const describedby = group.getAttribute("aria-describedby") as string; - const title = document.getElementById(describedby)!; - const items = group.querySelectorAll("[role=menuitem]"); - - return { - title: title.textContent!.trim(), - items: Array.from(items).map((item) => ({ - text: document.getElementById( - item.getAttribute("aria-labelledby")!, - )!.textContent, - highlight: item.getAttribute("aria-selected") === "true", - })), - }; - }); - }); - } - - async getBackgroundColor(): Promise { - return this.consoleFrame().evaluate(() => { - const input = document.querySelector("input")!; - return window.getComputedStyle(input).backgroundColor; - }); - } - - async getErrorMessage(): Promise { - return this.consoleFrame().textContent("[role=alert]"); - } - - private consoleFrame() { - return this.page.frame("vimmatic-console-frame"); - } -} diff --git a/e2e/lib/Selection.ts b/e2e/lib/Selection.ts deleted file mode 100644 index c098187c..00000000 --- a/e2e/lib/Selection.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { Page } from "@playwright/test"; - -type Range = { - from: number; - to: number; -}; - -export default class Selection { - private readonly page: Page; - - constructor(page: Page) { - this.page = page; - } - - getRange(): Promise { - return this.page.evaluate(() => { - const selection = window.getSelection(); - return { from: selection.anchorOffset, to: selection.focusOffset }; - }); - } -} diff --git a/e2e/lib/WebExtPage.ts b/e2e/lib/WebExtPage.ts deleted file mode 100644 index 8b4b06cb..00000000 --- a/e2e/lib/WebExtPage.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { Page, Keyboard } from "@playwright/test"; -import Console from "./Console"; -import Selection from "./Selection"; - -export default class WebExtPage { - private readonly page: Page; - readonly console: Console; - readonly selection: Selection; - readonly keyboard: Keyboard; - - constructor(page: Page) { - this.page = page; - this.console = new Console(page); - this.selection = new Selection(page); - this.keyboard = page.keyboard; - } - - async goto(url: string) { - await this.page.goto(url); - await new Promise((resolve) => setTimeout(resolve, 1000)); - } - - evaluate(code: unknown): Promise { - return this.page.evaluate(code as any); - } - - locator(selector: string) { - return this.page.locator(selector); - } - - waitForNavigation() { - return this.page.waitForNavigation; - } - - frame(selector: string) { - return this.page.frame(selector); - } - - async reload() { - await this.page.reload(); - await new Promise((resolve) => setTimeout(resolve, 1000)); - } - - url() { - return this.page.url(); - } -} diff --git a/e2e/lib/fixture.ts b/e2e/lib/fixture.ts index 6e4abb8e..fe3e122b 100644 --- a/e2e/lib/fixture.ts +++ b/e2e/lib/fixture.ts @@ -1,11 +1,12 @@ -import { createFixture } from "playwright-webextext"; +import { createFixture, withExtension } from "playwright-webextext"; import { connect } from "webext-agent"; -import WebExtPage from "./WebExtPage"; +import fs from "fs/promises"; +import os from "os"; +import path from "path"; type Browser = typeof browser; type Fixture = { - page: WebExtPage; api: Browser; }; @@ -14,16 +15,55 @@ const addonPath = "/tmp/vimmatic-mixedin"; const { test: base, expect } = createFixture(addonPath); export const test = base.extend({ - page: async ({ page: base }, use) => { - const page = new WebExtPage(base); - await page.goto("about:blank"); - await use(page); - }, - - api: async ({ page: _ }, use) => { - const webext = await connect("127.0.0.1:12345"); + api: async ({ context: _ }, use) => { + const webext = await connect("vimmatic@i-beam.org"); await use(webext); }, + context: [ + async ({ playwright, browserName }, use) => { + const userDataDir = await fs.mkdtemp( + path.join(os.tmpdir(), `playwright_${browserName}dev_profile-`), + ); + const browserType = withExtension(playwright[browserName], addonPath); + const newContext = await browserType.launchPersistentContext( + userDataDir, + { + // headless: false, + }, + ); + + await use(newContext); + await newContext.close(); + await fs.rm(userDataDir, { recursive: true }); + }, + { scope: "test" }, + ], + page: [ + async ({ page }, use) => { + const originalGoto = page.goto.bind(page); + page.goto = async (url: string, options?: { waitUntil?: unknown }) => { + const resp = await originalGoto(url, options); + if (typeof options?.waitUntil === "undefined") { + await page + .locator( + "head[data-vimmatic-content-status='ready'][data-vimmatic-console-status='ready']", + ) + .waitFor({ state: "attached" }); + } + return resp; + }; + + const originalType = page.keyboard.type.bind(page.keyboard); + page.keyboard.type = async ( + text: string, + options?: { delay?: number }, + ) => { + await originalType(text, { delay: options?.delay ?? 10 }); + }; + await use(page); + }, + { scope: "test" }, + ], }); export { expect }; diff --git a/e2e/mark.test.ts b/e2e/mark.test.ts index 614ee388..c64e23d8 100644 --- a/e2e/mark.test.ts +++ b/e2e/mark.test.ts @@ -18,7 +18,7 @@ test("should set a local mark and jump to it", async ({ page }) => { await page.evaluate(() => window.scrollBy(0, 500)); await page.keyboard.type("'a", { delay: 10 }); - await expect.poll(() => page.evaluate(() => window.pageYOffset)).toBe(200); + await expect.poll(() => page.evaluate(() => window.scrollY)).toBe(200); }); test.fixme( @@ -40,7 +40,7 @@ test.fixme( await page1.keyboard.type("mA"); await page1.evaluate(() => window.scrollBy(0, 500)); await page1.keyboard.type("'A"); - await expect.poll(() => page1.evaluate(() => window.pageYOffset)).toBe(200); + await expect.poll(() => page1.evaluate(() => window.scrollY)).toBe(200); await page2.keyboard.type("'A"); diff --git a/e2e/options.test.ts b/e2e/options.test.ts index 8cc7f572..9d0c847a 100644 --- a/e2e/options.test.ts +++ b/e2e/options.test.ts @@ -1,18 +1,9 @@ -/* -import * as path from "path"; -import * as assert from "assert"; +import { test } from "./lib/fixture"; -import TestServer from "./lib/TestServer"; -import eventually from "./eventually"; -import { Builder, Lanthan } from "lanthan"; -import { WebDriver } from "selenium-webdriver"; -import Page from "./lib/Page"; -import OptionPage from "./lib/OptionPage"; - -describe("options page", () => { +test.fixme("options page", () => { const server = new TestServer().receiveContent( "/", - `` + ``, ); let lanthan: Lanthan; let webdriver: WebDriver; @@ -51,7 +42,7 @@ describe("options page", () => { assert.strictEqual(settings.source, "json"); assert.strictEqual( settings.json, - '{ "blacklist": [ "https://example.com" ] } ' + '{ "blacklist": [ "https://example.com" ] } ', ); await jsonPage.updateSettings(`invalid json`); @@ -60,7 +51,7 @@ describe("options page", () => { assert.strictEqual(settings.source, "json"); assert.strictEqual( settings.json, - '{ "blacklist": [ "https://example.com" ] } ' + '{ "blacklist": [ "https://example.com" ] } ', ); const message = await jsonPage.getErrorMessage(); @@ -71,7 +62,7 @@ describe("options page", () => { const optionPage = await OptionPage.open(lanthan); const jsonPage = await optionPage.asJSONOptionPage(); await jsonPage.updateSettings( - `{ "keymaps": { "zz": { "type": "scroll.vertically", "count": 10 } } }` + `{ "keymaps": { "zz": { "type": "scroll.vertically", "count": 10 } } }`, ); await browser.tabs.create({ url: server.url(), active: false }); @@ -88,4 +79,3 @@ describe("options page", () => { }); }); }); -*/ diff --git a/e2e/scroll.spec.ts b/e2e/scroll.spec.ts new file mode 100644 index 00000000..2e0df0be --- /dev/null +++ b/e2e/scroll.spec.ts @@ -0,0 +1,72 @@ +import type { Page } from "@playwright/test"; +import { test, expect } from "./lib/fixture"; +import { newScrollableServer } from "./lib/servers"; + +const server = newScrollableServer(); +const expectScrollX = (page: Page) => + expect.poll(() => page.evaluate(() => window.scrollX)); +const expectScrollY = (page: Page) => + expect.poll(() => page.evaluate(() => window.scrollY)); + +test.beforeAll(async () => { + await server.start(); +}); + +test.afterAll(async () => { + await server.stop(); +}); + +test("scroll by j/k/h/l", async ({ page }) => { + await page.goto(server.url()); + await page.evaluate(() => window.scrollBy(1000, 1000)); + + await page.keyboard.press("j"); + await expectScrollY(page).toBe(1064); + + await page.keyboard.press("k"); + await expectScrollY(page).toBe(1000); + + await page.keyboard.press("l"); + await expectScrollX(page).toBe(1064); + + await page.keyboard.press("h"); + await expectScrollX(page).toBe(1000); +}); + +test("scrolls top/bottom by gg/G/0/$", async ({ page }) => { + await page.goto(server.url()); + await page.evaluate(() => window.scrollBy(1000, 1000)); + + await page.keyboard.press("g"); + await page.keyboard.press("g"); + await expectScrollY(page).toBe(0); + + await page.keyboard.press("Shift+G"); + await expectScrollY(page).toBeGreaterThan(5000); + + await page.keyboard.press("0"); + await expectScrollX(page).toBe(0); + + await page.keyboard.press("$"); + await expectScrollX(page).toBeGreaterThan(5000); +}); + +test("scroll by pages", async ({ page }) => { + await page.goto(server.url()); + const pageHeight = await page.evaluate( + () => window.document.documentElement.clientHeight, + ); + + await page.evaluate(() => window.scrollBy(1000, 1000)); + await page.keyboard.press("Control+u"); + await expectScrollY(page).toBe(1000 - pageHeight / 2); + + await page.keyboard.press("Control+d"); + await expectScrollY(page).toBe(1000); + + await page.keyboard.press("Control+b"); + await expectScrollY(page).toBe(1000 - pageHeight); + + await page.keyboard.press("Control+f"); + await expectScrollY(page).toBe(1000); +}); diff --git a/e2e/scroll.test.ts b/e2e/scroll.test.ts deleted file mode 100644 index dc005dce..00000000 --- a/e2e/scroll.test.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { test, expect } from "./lib/fixture"; -import { newScrollableServer } from "./lib/servers"; - -const server = newScrollableServer(); - -test.beforeAll(async () => { - await server.start(); -}); - -test.afterAll(async () => { - await server.stop(); -}); - -test("scrolls up by j", async ({ page }) => { - await page.goto(server.url()); - await page.keyboard.press("j"); - - await expect.poll(() => page.evaluate(() => window.pageYOffset)).toBe(64); -}); - -test("scrolls down by k", async ({ page }) => { - await page.goto(server.url()); - await page.evaluate(() => window.scrollBy(0, 100)); - await page.keyboard.press("k"); - - await expect.poll(() => page.evaluate(() => window.pageYOffset)).toBe(36); -}); - -test("scrolls left by h", async ({ page }) => { - await page.goto(server.url()); - await page.evaluate(() => window.scrollBy(100, 0)); - await page.keyboard.press("h"); - - await expect.poll(() => page.evaluate(() => window.pageXOffset)).toBe(36); -}); - -test("scrolls left by l", async ({ page }) => { - await page.goto(server.url()); - await page.evaluate(() => window.scrollBy(100, 0)); - await page.keyboard.press("l"); - - await expect.poll(() => page.evaluate(() => window.pageXOffset)).toBe(164); -}); - -test("scrolls top by gg", async ({ page }) => { - await page.goto(server.url()); - await page.evaluate(() => window.scrollBy(0, 100)); - await page.keyboard.press("g"); - await page.keyboard.press("g"); - - await expect.poll(() => page.evaluate(() => window.pageYOffset)).toBe(0); -}); - -test("scrolls bottom by G", async ({ page }) => { - await page.goto(server.url()); - await page.keyboard.press("Shift+G"); - - await expect - .poll(() => page.evaluate(() => window.pageYOffset)) - .toBeGreaterThan(5000); -}); - -test("scrolls bottom by 0", async ({ page }) => { - await page.goto(server.url()); - await page.evaluate(() => window.scrollBy(100, 100)); - await page.keyboard.press("0"); - - await expect.poll(() => page.evaluate(() => window.pageXOffset)).toBe(0); -}); - -test("scrolls bottom by $", async ({ page }) => { - await page.goto(server.url()); - await page.evaluate(() => window.scrollBy(100, 100)); - await page.keyboard.press("$"); - - await expect - .poll(() => page.evaluate(() => window.pageXOffset)) - .toBeGreaterThanOrEqual(5000); -}); - -test("scrolls bottom by ", async ({ page }) => { - await page.goto(server.url()); - await page.evaluate(() => window.scrollBy(0, 1000)); - await page.keyboard.press("Control+u"); - - await expect - .poll(async () => { - const scrollY = await page.evaluate(() => window.pageYOffset); - const pageHeight = await page.evaluate( - () => window.document.documentElement.clientHeight, - ); - return Math.abs(scrollY - (1000 - pageHeight / 2)); - }) - .toBeLessThan(5); -}); - -test("scrolls bottom by ", async ({ page }) => { - await page.goto(server.url()); - await page.evaluate(() => window.scrollBy(0, 1000)); - await page.keyboard.press("Control+d"); - - await expect - .poll(async () => { - const scrollY = await page.evaluate(() => window.pageYOffset); - const pageHeight = await page.evaluate( - () => window.document.documentElement.clientHeight, - ); - return Math.abs(scrollY - (1000 + pageHeight / 2)); - }) - .toBeLessThan(5); -}); - -test("scrolls bottom by ", async ({ page }) => { - await page.goto(server.url()); - await page.evaluate(() => window.scrollBy(0, 1000)); - await page.keyboard.press("Control+b"); - - await expect - .poll(async () => { - const scrollY = await page.evaluate(() => window.pageYOffset); - const pageHeight = await page.evaluate( - () => window.document.documentElement.clientHeight, - ); - return Math.abs(scrollY - (1000 - pageHeight)); - }) - .toBeLessThan(5); -}); - -test("scrolls bottom by ", async ({ page }) => { - await page.goto(server.url()); - await page.evaluate(() => window.scrollBy(0, 1000)); - await page.keyboard.press("Control+f"); - - await expect - .poll(async () => { - const scrollY = await page.evaluate(() => window.pageYOffset); - const pageHeight = await page.evaluate( - () => window.document.documentElement.clientHeight, - ); - return Math.abs(scrollY - (1000 + pageHeight)); - }) - .toBeLessThan(5); -}); diff --git a/e2e/tab.test.ts b/e2e/tab.test.ts index 052998b7..5de67a82 100644 --- a/e2e/tab.test.ts +++ b/e2e/tab.test.ts @@ -1,235 +1,150 @@ import { test, expect } from "./lib/fixture"; +import { newNopServer } from "./lib/servers"; + +const server = newNopServer(); +let windowId: number; + +test.beforeAll(async () => { + await server.start(); +}); + +test.afterAll(async () => { + await server.stop(); +}); + +test.beforeEach(async ({ api, page }) => { + windowId = (await api.windows.getCurrent()).id; + await setupTabs(api); + await page.goto(server.url("2")); +}); const setupTabs = async (api: typeof browser) => { - const { id: windowId } = await api.windows.getCurrent(); for (const i of [0, 1, 3, 4]) { await api.tabs.create({ windowId, - url: `about:blank#${i}`, + url: server.url(`${i}`), active: false, index: i, }); } }; -test("deletes tab and selects right by d", async ({ page, api }) => { - const { id: windowId } = await api.windows.getCurrent(); - await setupTabs(api); - - await page.keyboard.press("d"); +test("select tab", async ({ page, api }) => { await expect - .poll(() => api.tabs.query({ windowId })) - .toMatchObject([ - { url: "about:blank#0" }, - { url: "about:blank#1" }, - { url: "about:blank#3", active: true }, - { url: "about:blank#4" }, - ]); -}); + .poll(() => api.tabs.query({ windowId, active: true })) + .toMatchObject([{ url: server.url("2") }]); -test("deletes tab and selects left by D", async ({ page, api }) => { - const { id: windowId } = await api.windows.getCurrent(); - await setupTabs(api); - - await page.keyboard.press("Shift+D"); + await page.keyboard.press("Shift+J"); + await page.keyboard.press("Shift+J"); await expect - .poll(() => api.tabs.query({ windowId })) - .toMatchObject([ - { url: "about:blank#0" }, - { url: "about:blank#1", active: true }, - { url: "about:blank#3" }, - { url: "about:blank#4" }, - ]); -}); + .poll(() => api.tabs.query({ windowId, active: true })) + .toMatchObject([{ url: server.url("4") }]); -test("deletes all tabs to the right by x$", async ({ page, api }) => { - const { id: windowId } = await api.windows.getCurrent(); - await setupTabs(api); - - await page.keyboard.press("x"); - await page.keyboard.press("$"); + await page.keyboard.press("Shift+K"); + await page.keyboard.press("Shift+K"); await expect - .poll(() => api.tabs.query({ windowId })) - .toMatchObject([ - { url: "about:blank#0" }, - { url: "about:blank#1" }, - { url: "about:blank", active: true }, - ]); -}); - -test("duplicates tab by zd", async ({ page, api }) => { - const { id: windowId } = await api.windows.getCurrent(); - - await page.keyboard.press("z"); - await page.keyboard.press("d"); + .poll(() => api.tabs.query({ windowId, active: true })) + .toMatchObject([{ url: server.url("2") }]); + await page.keyboard.press("g"); + await page.keyboard.press("0"); await expect - .poll(() => api.tabs.query({ windowId })) - .toMatchObject([{ url: "about:blank" }, { url: "about:blank" }]); -}); - -test("makes pinned by zp", async ({ page, api }) => { - const { id: windowId } = await api.windows.getCurrent(); - - await page.keyboard.press("z"); - await page.keyboard.press("p"); + .poll(() => api.tabs.query({ windowId, active: true })) + .toMatchObject([{ url: server.url("0"), active: true }]); + await page.keyboard.press("g"); + await page.keyboard.press("$"); await expect - .poll(() => api.tabs.query({ windowId })) - .toMatchObject([{ url: "about:blank", pinned: true }]); -}); - -test.fixme("switches to reader view", async ({ page }) => { - await page.keyboard.press("z"); - await page.keyboard.press("r"); + .poll(() => api.tabs.query({ windowId, active: true })) + .toMatchObject([{ url: server.url("4"), active: true }]); + await page.keyboard.press("Control+6"); await expect - .poll(() => page.console.getErrorMessage()) - .toBe("The specified tab cannot be placed into reader mode."); + .poll(() => api.tabs.query({ windowId, active: true })) + .toMatchObject([{ url: server.url("0"), active: true }]); }); -test("selects previous tab by K", async ({ page, api }) => { - const { id: windowId } = await api.windows.getCurrent(); - await setupTabs(api); - - await page.keyboard.press("Shift+K"); - await expect - .poll(() => api.tabs.query({ windowId })) - .toMatchObject([ - { url: "about:blank#0" }, - { url: "about:blank#1", active: true }, - { url: "about:blank" }, - { url: "about:blank#3" }, - { url: "about:blank#4" }, - ]); - - await page.keyboard.press("Shift+K"); - await page.keyboard.press("Shift+K"); +test("deletes tab and selects right by d", async ({ page, api }) => { + await page.keyboard.press("d"); await expect .poll(() => api.tabs.query({ windowId })) .toMatchObject([ - { url: "about:blank#0" }, - { url: "about:blank#1" }, - { url: "about:blank" }, - { url: "about:blank#3" }, - { url: "about:blank#4", active: true }, + { url: server.url("0") }, + { url: server.url("1") }, + { url: server.url("3"), active: true }, + { url: server.url("4") }, ]); }); -test("selects next tab by J", async ({ page, api }) => { - const { id: windowId } = await api.windows.getCurrent(); - await setupTabs(api); - - await page.keyboard.press("Shift+J"); - await expect - .poll(() => api.tabs.query({ windowId })) - .toMatchObject([ - { url: "about:blank#0" }, - { url: "about:blank#1" }, - { url: "about:blank" }, - { url: "about:blank#3", active: true }, - { url: "about:blank#4" }, - ]); - - await page.keyboard.press("Shift+J"); - await page.keyboard.press("Shift+J"); +test("deletes tab and selects left by D", async ({ page, api }) => { + await page.keyboard.press("Shift+D"); await expect .poll(() => api.tabs.query({ windowId })) .toMatchObject([ - { url: "about:blank#0", active: true }, - { url: "about:blank#1" }, - { url: "about:blank" }, - { url: "about:blank#3" }, - { url: "about:blank#4" }, + { url: server.url("0") }, + { url: server.url("1"), active: true }, + { url: server.url("3") }, + { url: server.url("4") }, ]); }); -test("selects first tab by g0 and last tab by g$", async ({ page, api }) => { - const { id: windowId } = await api.windows.getCurrent(); - await setupTabs(api); - - await page.keyboard.press("g"); - await page.keyboard.press("0"); - await expect - .poll(() => api.tabs.query({ windowId })) - .toMatchObject([ - { url: "about:blank#0", active: true }, - { url: "about:blank#1" }, - { url: "about:blank" }, - { url: "about:blank#3" }, - { url: "about:blank#4" }, - ]); - - await page.keyboard.press("g"); +test("deletes all tabs to the right by x$", async ({ page, api }) => { + await page.keyboard.press("x"); await page.keyboard.press("$"); await expect .poll(() => api.tabs.query({ windowId })) .toMatchObject([ - { url: "about:blank#0" }, - { url: "about:blank#1" }, - { url: "about:blank" }, - { url: "about:blank#3" }, - { url: "about:blank#4", active: true }, + { url: server.url("0") }, + { url: server.url("1") }, + { url: server.url("2"), active: true }, ]); }); -test("selects last selected tab by Control+6", async ({ page, api }) => { - const { id: windowId } = await api.windows.getCurrent(); - await setupTabs(api); - - await page.keyboard.press("Shift+J"); +test("duplicate and make pinned", async ({ page, api }) => { + await page.keyboard.type("zd"); await expect .poll(() => api.tabs.query({ windowId })) .toMatchObject([ - { url: "about:blank#0" }, - { url: "about:blank#1" }, - { url: "about:blank" }, - { url: "about:blank#3", active: true }, - { url: "about:blank#4" }, + { url: server.url("0") }, + { url: server.url("1") }, + { url: server.url("2") }, + { url: server.url("2") }, + { url: server.url("3") }, + { url: server.url("4") }, ]); - await page.keyboard.press("Control+6"); + await page.keyboard.type("zp"); await expect - .poll(() => api.tabs.query({ windowId })) - .toMatchObject([ - { url: "about:blank#0" }, - { url: "about:blank#1" }, - { url: "about:blank", active: true }, - { url: "about:blank#3" }, - { url: "about:blank#4" }, - ]); + .poll(() => api.tabs.query({ windowId, pinned: true })) + .toMatchObject([{ url: server.url("2") }]); }); test("reopen tab by u", async ({ page, api }) => { - const { id: windowId } = await api.windows.getCurrent(); - await setupTabs(api); - const tabs = await api.tabs.query({ windowId }); await api.tabs.remove(tabs[4].id); + await expect .poll(() => api.tabs.query({ windowId })) .toMatchObject([ - { url: "about:blank#0" }, - { url: "about:blank#1" }, - { url: "about:blank" }, - { url: "about:blank#3" }, + { url: server.url("0") }, + { url: server.url("1") }, + { url: server.url("2") }, + { url: server.url("3") }, ]); await page.keyboard.press("u"); await expect .poll(() => api.tabs.query({ windowId })) .toMatchObject([ - { url: "about:blank#0" }, - { url: "about:blank#1" }, - { url: "about:blank" }, - { url: "about:blank#3" }, - { url: "about:blank#4" }, + { url: server.url("0") }, + { url: server.url("1") }, + { url: server.url("2") }, + { url: server.url("3") }, + { url: server.url("4") }, ]); }); -test("does not delete pinned tab by !d", async ({ page, api }) => { - const { id: windowId } = await api.windows.getCurrent(); - await setupTabs(api); +test("delete a pinned tab", async ({ page, api }) => { const tabs = await api.tabs.query({ windowId }); await api.tabs.update(tabs[2].id, { pinned: true }); @@ -237,11 +152,11 @@ test("does not delete pinned tab by !d", async ({ page, api }) => { await expect .poll(() => api.tabs.query({ windowId })) .toMatchObject([ - { url: "about:blank", pinned: true, active: true }, - { url: "about:blank#0" }, - { url: "about:blank#1" }, - { url: "about:blank#3" }, - { url: "about:blank#4" }, + { url: server.url("2"), pinned: true, active: true }, + { url: server.url("0") }, + { url: server.url("1") }, + { url: server.url("3") }, + { url: server.url("4") }, ]); await page.keyboard.press("!"); @@ -249,23 +164,16 @@ test("does not delete pinned tab by !d", async ({ page, api }) => { await expect .poll(() => api.tabs.query({ windowId })) .toMatchObject([ - { url: "about:blank#0" }, - { url: "about:blank#1" }, - { url: "about:blank#3" }, - { url: "about:blank#4" }, + { url: server.url("0") }, + { url: server.url("1") }, + { url: server.url("3") }, + { url: server.url("4") }, ]); }); test("opens view-source by gf", async ({ page, api }) => { - const { id: windowId } = await api.windows.getCurrent(); - - await page.keyboard.press("g"); - await page.keyboard.press("f"); - + await page.keyboard.type("gf"); await expect - .poll(() => api.tabs.query({ windowId })) - .toMatchObject([ - { url: "about:blank" }, - { url: "view-source:about:blank" }, - ]); + .poll(() => api.tabs.query({ windowId, active: true })) + .toMatchObject([{ url: "view-source:" + server.url("2") }]); }); diff --git a/e2e/zoom.test.ts b/e2e/zoom.test.ts index 5c0e3f21..aef33478 100644 --- a/e2e/zoom.test.ts +++ b/e2e/zoom.test.ts @@ -1,26 +1,29 @@ import { test, expect } from "./lib/fixture"; +import { newNopServer } from "./lib/servers"; -test("should zoom in by zi", async ({ page, api }) => { - const tab = await api.tabs.getCurrent(); - const before = await api.tabs.getZoom(tab.id); +const server = newNopServer(); - await page.keyboard.type("zi"); +test.beforeAll(async () => { + await server.start(); +}); - await expect.poll(() => api.tabs.getZoom(tab.id)).toBeGreaterThan(before); +test.afterAll(async () => { + await server.stop(); }); -test("should zoom out by zo", async ({ page, api }) => { +test("should zoom-in and zoom-out", async ({ page, api }) => { + await page.goto(server.url()); + const tab = await api.tabs.getCurrent(); const before = await api.tabs.getZoom(tab.id); - await page.keyboard.type("zo"); + await page.keyboard.type("zi"); + await expect.poll(() => api.tabs.getZoom(tab.id)).toBeGreaterThan(before); + await page.keyboard.type("zo"); + await page.keyboard.type("zo"); await expect.poll(() => api.tabs.getZoom(tab.id)).toBeLessThan(before); -}); -test("should reset zoom by zz", async ({ page, api }) => { - const tab = await api.tabs.getCurrent(); - await api.tabs.setZoom(tab.id, 2); await page.keyboard.type("zz"); await expect.poll(() => api.tabs.getZoom(tab.id)).toBe(1); diff --git a/package.json b/package.json index 11a86c00..8baa5b65 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ }, "devDependencies": { "@jest/types": "^29.6.3", - "@playwright/test": "1.26.0", + "@playwright/test": "1.40.1", "@testing-library/react-hooks": "^8.0.1", "@types/chrome": "^0.0.233", "@types/jest": "^29.5.5", @@ -61,13 +61,12 @@ "jest-environment-jsdom": "^29.7.0", "jszip": "^3.7.0", "nodemon": "^2.0.22", - "playwright": "1.26.0", - "playwright-webextext": "^0.0.3", + "playwright-webextext": "^0.0.4", "prettier": "3.1.1", "prettier-eslint": "16.2.0", "react-test-renderer": "18.2.0", "ts-node": "^10.7.0", - "webext-agent": "^0.1.0" + "webext-agent": "^0.2.0" }, "engines": { "node": "20.1.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 64f6db07..b5b4f256 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,8 +41,8 @@ devDependencies: specifier: ^29.6.3 version: 29.6.3 '@playwright/test': - specifier: 1.26.0 - version: 1.26.0 + specifier: 1.40.1 + version: 1.40.1 '@testing-library/react-hooks': specifier: ^8.0.1 version: 8.0.1(@types/react@18.2.46)(react-dom@18.2.0)(react-test-renderer@18.2.0)(react@18.2.0) @@ -106,12 +106,9 @@ devDependencies: nodemon: specifier: ^2.0.22 version: 2.0.22 - playwright: - specifier: 1.26.0 - version: 1.26.0 playwright-webextext: - specifier: ^0.0.3 - version: 0.0.3(@playwright/test@1.26.0)(playwright@1.26.0) + specifier: ^0.0.4 + version: 0.0.4(@playwright/test@1.40.1) prettier: specifier: 3.1.1 version: 3.1.1 @@ -125,8 +122,8 @@ devDependencies: specifier: ^10.7.0 version: 10.7.0(@types/node@20.6.0)(typescript@5.2.2) webext-agent: - specifier: ^0.1.0 - version: 0.1.0 + specifier: ^0.2.0 + version: 0.2.0 packages: @@ -1999,20 +1996,22 @@ packages: resolution: {integrity: sha512-dj7vjIn1Ar8sVXj2yAXiMNCJDmS9MQ9XMlIecX2dIzzhjSHCyKo4DdXjXMs7wKW2kj6yvVRSpuQjOZ3YLrh56w==} dev: true + /@fastify/error@3.4.1: + resolution: {integrity: sha512-wWSvph+29GR783IhmvdwWnN4bUxTD01Vm5Xad4i7i1VuAOItLvbPAb69sb0IQ2N57yprvhNIwAP5B6xfKTmjmQ==} + dev: true + /@fastify/fast-json-stringify-compiler@4.3.0: resolution: {integrity: sha512-aZAXGYo6m22Fk1zZzEUKBvut/CIIQe/BapEORnxiD5Qr0kPHqqI69NtEMCme74h+at72sPhbkb4ZrLd1W3KRLA==} dependencies: fast-json-stringify: 5.8.0 dev: true - /@fastify/type-provider-typebox@2.3.0(@sinclair/typebox@0.24.46)(fastify@4.23.2): - resolution: {integrity: sha512-arEH1FL6CNTgctoVw++jNPJN9DdzUe7rJlb3XJyw7rAGiT2m402P4GMFVhtDqndQzEJN37UJXVYbCR9D0AahgQ==} + /@fastify/type-provider-typebox@4.0.0(@sinclair/typebox@0.24.51): + resolution: {integrity: sha512-kTlN0saC/+xhcQPyBjb3YONQAMjiD/EHlCRjQjsr5E3NFjS5K8ZX5LGzXYDRjSa+sV4y8gTL5Q7FlObePv4iTA==} peerDependencies: - '@sinclair/typebox': ^0.24.1 - fastify: ^4.0.0 + '@sinclair/typebox': '>=0.26 <=0.32' dependencies: - '@sinclair/typebox': 0.24.46 - fastify: 4.23.2 + '@sinclair/typebox': 0.24.51 dev: true /@humanwhocodes/config-array@0.11.13: @@ -2356,17 +2355,16 @@ packages: engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} dev: true - /@playwright/test@1.26.0: - resolution: {integrity: sha512-D24pu1k/gQw3Lhbpc38G5bXlBjGDrH5A52MsrH12wz6ohGDeQ+aZg/JFSEsT/B3G8zlJe/EU4EkJK74hpqsjEg==} - engines: {node: '>=14'} + /@playwright/test@1.40.1: + resolution: {integrity: sha512-EaaawMTOeEItCRvfmkI9v6rBkF1svM8wjl/YPRrg2N2Wmp+4qJYkWtJsbew1szfKKDm6fPLy4YAanBhIlf9dWw==} + engines: {node: '>=16'} hasBin: true dependencies: - '@types/node': 20.6.0 - playwright-core: 1.26.0 + playwright: 1.40.1 dev: true - /@sinclair/typebox@0.24.46: - resolution: {integrity: sha512-ng4ut1z2MCBhK/NwDVwIQp3pAUOCs/KNaW3cBxdFB2xTDrOuo1xuNmpr/9HHFhxqIvHrs1NTH3KJg6q+JSy1Kw==} + /@sinclair/typebox@0.24.51: + resolution: {integrity: sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==} dev: true /@sinclair/typebox@0.27.8: @@ -3046,11 +3044,12 @@ packages: - supports-color dev: true - /axios@0.27.2: - resolution: {integrity: sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==} + /axios@1.6.7: + resolution: {integrity: sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==} dependencies: - follow-redirects: 1.15.2 + follow-redirects: 1.15.5 form-data: 4.0.0 + proxy-from-env: 1.1.0 transitivePeerDependencies: - debug dev: true @@ -3509,8 +3508,8 @@ packages: engines: {node: '>= 6'} dev: false - /commander@9.4.1: - resolution: {integrity: sha512-5EEkTNyHNGFPD2H+c/dXXfQZYa/scCKasxWcXJaWnNJ99pnQN9Vnmqow+p+PlFPE63Q6mThaZws1T+HxfpgtPw==} + /commander@9.5.0: + resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} engines: {node: ^12.20.0 || >=14} dev: true @@ -4263,6 +4262,10 @@ packages: resolution: {integrity: sha512-Xbc4XcysUXcsP5aHUU7Nq3OwvHq97C+WnbkeIefpeYLX+ryzFJlU6OStFJhs6Ol0LkUGpcK+wL0JwfM+FCU5IA==} dev: true + /fast-content-type-parse@1.1.0: + resolution: {integrity: sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==} + dev: true + /fast-decode-uri-component@1.0.1: resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} dev: true @@ -4343,6 +4346,29 @@ packages: - supports-color dev: true + /fastify@4.25.2: + resolution: {integrity: sha512-SywRouGleDHvRh054onj+lEZnbC1sBCLkR0UY3oyJwjD4BdZJUrxBqfkfCaqn74pVCwBaRHGuL3nEWeHbHzAfw==} + dependencies: + '@fastify/ajv-compiler': 3.5.0 + '@fastify/error': 3.4.1 + '@fastify/fast-json-stringify-compiler': 4.3.0 + abstract-logging: 2.0.1 + avvio: 8.2.1 + fast-content-type-parse: 1.1.0 + fast-json-stringify: 5.8.0 + find-my-way: 7.7.0 + light-my-request: 5.11.0 + pino: 8.17.2 + process-warning: 3.0.0 + proxy-addr: 2.0.7 + rfdc: 1.3.0 + secure-json-parse: 2.7.0 + semver: 7.5.4 + toad-cache: 3.7.0 + transitivePeerDependencies: + - supports-color + dev: true + /fastq@1.16.0: resolution: {integrity: sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==} dependencies: @@ -4388,6 +4414,15 @@ packages: safe-regex2: 2.0.0 dev: true + /find-my-way@7.7.0: + resolution: {integrity: sha512-+SrHpvQ52Q6W9f3wJoJBbAQULJuNEEQwBvlvYwACDhBTLOTMiQ0HYWh4+vC3OivGP2ENcTI1oKlFA2OepJNjhQ==} + engines: {node: '>=14'} + dependencies: + fast-deep-equal: 3.1.3 + fast-querystring: 1.0.0 + safe-regex2: 2.0.0 + dev: true + /find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -4417,8 +4452,8 @@ packages: resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==} dev: true - /follow-redirects@1.15.2: - resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==} + /follow-redirects@1.15.5: + resolution: {integrity: sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==} engines: {node: '>=4.0'} peerDependencies: debug: '*' @@ -4466,6 +4501,14 @@ packages: /fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + /fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + /fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -6377,6 +6420,23 @@ packages: thread-stream: 2.2.0 dev: true + /pino@8.17.2: + resolution: {integrity: sha512-LA6qKgeDMLr2ux2y/YiUt47EfgQ+S9LznBWOJdN3q1dx2sv0ziDLUBeVpyVv17TEcGCBuWf0zNtg3M5m1NhhWQ==} + hasBin: true + dependencies: + atomic-sleep: 1.0.0 + fast-redact: 3.1.2 + on-exit-leak-free: 2.1.0 + pino-abstract-transport: 1.1.0 + pino-std-serializers: 6.0.0 + process-warning: 3.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.4.0 + sonic-boom: 3.8.0 + thread-stream: 2.2.0 + dev: true + /pirates@4.0.6: resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} engines: {node: '>= 6'} @@ -6389,29 +6449,35 @@ packages: find-up: 4.1.0 dev: true - /playwright-core@1.26.0: - resolution: {integrity: sha512-p8huU8eU4gD3VkJd3DA1nA7R3XA6rFvFL+1RYS96cSljCF2yJE9CWEHTPF4LqX8KN9MoWCrAfVKP5381X3CZqg==} - engines: {node: '>=14'} + /playwright-core@1.40.1: + resolution: {integrity: sha512-+hkOycxPiV534c4HhpfX6yrlawqVUzITRKwHAmYfmsVreltEl6fAZJ3DPfLMOODw0H3s1Itd6MDCWmP1fl/QvQ==} + engines: {node: '>=16'} hasBin: true dev: true - /playwright-webextext@0.0.3(@playwright/test@1.26.0)(playwright@1.26.0): - resolution: {integrity: sha512-qcnMZES3vbnEfCrOobsaFrT+BBlRgCmKBCzhoRxPkdC9C5KSOYdROikuTHRoe/ez5v1Eh02szQjwRBStOkDF6g==} + /playwright-webextext@0.0.4(@playwright/test@1.40.1): + resolution: {integrity: sha512-B5uIZSRtH6wi5HPhEwbEkZxEoAY5bUKCjqVW2qCsAYWQTQZ4ULOfsyglI+gfiohxrAzHT+mtkZMXZvHTtunsDg==} + engines: {node: '20'} peerDependencies: - '@playwright/test': 1.26.0 - playwright: 1.26.0 + '@playwright/test': '>=1.0.0' + playwright: '>=1.0.0' + peerDependenciesMeta: + '@playwright/test': + optional: true + playwright: + optional: true dependencies: - '@playwright/test': 1.26.0 - playwright: 1.26.0 + '@playwright/test': 1.40.1 dev: true - /playwright@1.26.0: - resolution: {integrity: sha512-XxTVlvFEYHdatxUkh1KiPq9BclNtFKMi3BgQnl/aactmhN4G9AkZUXwt0ck6NDAOrDFlfibhbM7A1kZwQJKSBw==} - engines: {node: '>=14'} + /playwright@1.40.1: + resolution: {integrity: sha512-2eHI7IioIpQ0bS1Ovg/HszsN/XKNwEG1kbzSDDmADpclKc7CyqkHw7Mg2JCz/bbCxg25QUPcjksoMW7JcIFQmw==} + engines: {node: '>=16'} hasBin: true - requiresBuild: true dependencies: - playwright-core: 1.26.0 + playwright-core: 1.40.1 + optionalDependencies: + fsevents: 2.3.2 dev: true /posix-character-classes@0.1.1: @@ -6492,6 +6558,10 @@ packages: resolution: {integrity: sha512-/1WZ8+VQjR6avWOgHeEPd7SDQmFQ1B5mC1eRXsCm5TarlNmx/wCsa5GEaxGm05BORRtyG/Ex/3xq3TuRvq57qg==} dev: true + /process-warning@3.0.0: + resolution: {integrity: sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==} + dev: true + /process@0.11.10: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} @@ -6521,6 +6591,10 @@ packages: ipaddr.js: 1.9.1 dev: true + /proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + dev: true + /psl@1.9.0: resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} dev: true @@ -6666,8 +6740,8 @@ packages: which-builtin-type: 1.1.3 dev: true - /regedit@5.0.0: - resolution: {integrity: sha512-4uSqj6Injwy5TPtXlE+1F/v2lOW/bMfCqNIAXyib4aG1ZwacG69oyK/yb6EF8KQRMhz7YINxkD+/HHc6i7YJtA==} + /regedit@5.1.2: + resolution: {integrity: sha512-pQpWqO/I40bMNoMO9kTQx3e5iK542kYcB/Z8X3Y7Hcri6ydc4KZ9ByUsEWFkBRMcwo+2irHuNK5s+pMGPr6VPw==} dependencies: debug: 4.3.4 if-async: 3.7.4 @@ -6929,6 +7003,10 @@ packages: resolution: {integrity: sha512-ZQruFgZnIWH+WyO9t5rWt4ZEGqCKPwhiw+YbzTwpmT9elgLrLcfuyUiSnwwjUiVy9r4VM3urtbNF1xmEh9IL2w==} dev: true + /secure-json-parse@2.7.0: + resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} + dev: true + /semver@5.7.1: resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==} hasBin: true @@ -7082,6 +7160,12 @@ packages: atomic-sleep: 1.0.0 dev: true + /sonic-boom@3.8.0: + resolution: {integrity: sha512-ybz6OYOUjoQQCQ/i4LU8kaToD8ACtYP+Cj5qd2AO36bwbdewxWJ3ArmJ2cr6AvxlL2o0PqnCcPGUgkILbfkaCA==} + dependencies: + atomic-sleep: 1.0.0 + dev: true + /source-map-js@1.0.2: resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} engines: {node: '>=0.10.0'} @@ -7407,6 +7491,11 @@ packages: engines: {node: '>=12'} dev: true + /toad-cache@3.7.0: + resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==} + engines: {node: '>=12'} + dev: true + /touch@3.1.0: resolution: {integrity: sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==} hasBin: true @@ -7701,20 +7790,23 @@ packages: makeerror: 1.0.12 dev: true - /webext-agent@0.1.0: - resolution: {integrity: sha512-2dnyCrb2IbtyJBY5gbX0wcjYmr/pQQRs4KD2KsSSB+Uju4A8iqr2KGF/BHvk+1BcaA1pe2n9QtuecMPrK1b0fg==} + /webext-agent@0.2.0: + resolution: {integrity: sha512-ySioVcFJo8+4sjL6XTiEeLmMAg2iwHfdXajZoj8VGwCu+0rRWVQBgx/rcq93gc9xNrCRPILg2Q4b0YXXLCKLXg==} + engines: {node: '20'} hasBin: true dependencies: - '@fastify/type-provider-typebox': 2.3.0(@sinclair/typebox@0.24.46)(fastify@4.23.2) - '@sinclair/typebox': 0.24.46 + '@fastify/type-provider-typebox': 4.0.0(@sinclair/typebox@0.24.51) + '@sinclair/typebox': 0.24.51 '@types/firefox-webext-browser': 94.0.1 '@types/uuid': 8.3.4 - axios: 0.27.2 - commander: 9.4.1 - fastify: 4.23.2 - regedit: 5.0.0 + axios: 1.6.7 + commander: 9.5.0 + fastify: 4.25.2 + pino: 8.17.2 + regedit: 5.1.2 tslib: 2.6.2 uuid: 8.3.2 + zod: 3.22.4 transitivePeerDependencies: - debug - supports-color @@ -7907,3 +7999,7 @@ packages: /zod@3.22.2: resolution: {integrity: sha512-wvWkphh5WQsJbVk1tbx1l1Ly4yg+XecD+Mq280uBGt9wa5BKSWf4Mhp6GmrkPixhMxmabYY7RbzlwVP32pbGCg==} dev: false + + /zod@3.22.4: + resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} + dev: true diff --git a/src/console/App.tsx b/src/console/App.tsx index 75ffb70b..3e2d4ae9 100644 --- a/src/console/App.tsx +++ b/src/console/App.tsx @@ -11,6 +11,7 @@ import { useGetCommandCompletion, useExecFind, useGetFindCompletion, + useSendReady, } from "./app/hooks"; import { useInvalidateStyle } from "./styles/hooks"; @@ -32,6 +33,7 @@ const App: React.FC = () => { const execFind = useExecFind(); const getCommandCompletions = useGetCommandCompletion(); const getFindCompletions = useGetFindCompletion(); + const sendReady = useSendReady(); const onExec = React.useCallback( (cmd: string) => { if (state.mode !== "prompt") { @@ -71,6 +73,8 @@ const App: React.FC = () => { port.onMessage.addListener((message: any) => { receiver.receive(message.type, message.args); }); + + sendReady(); }, []); if (state.mode === "prompt") { diff --git a/src/console/app/hooks.ts b/src/console/app/hooks.ts index e08c17d3..0f03ff32 100644 --- a/src/console/app/hooks.ts +++ b/src/console/app/hooks.ts @@ -104,3 +104,10 @@ export const useGetFindCompletion = () => { }, []); return execFind; }; + +export const useSendReady = () => { + const sendReady = React.useCallback(() => { + windowMessageSender.send("console.ready"); + }, []); + return sendReady; +}; diff --git a/src/content/Application.ts b/src/content/Application.ts index d6efb0a7..b42c86ba 100644 --- a/src/content/Application.ts +++ b/src/content/Application.ts @@ -4,6 +4,7 @@ import ContentMessageListener from "./messaging/ContentMessageListener"; import KeyController from "./controllers/KeyController"; import SettingsController from "./controllers/SettingsController"; import InputDriver from "./InputDriver"; +import ReadyStatusPresenter from "./presenters/ReadyStatusPresenter"; @injectable() export default class Application { @@ -17,6 +18,8 @@ export default class Application { private readonly keyController: KeyController, @inject(SettingsController) private readonly settingsController: SettingsController, + @inject("ReadyStatusPresenter") + private readonly readyStatusPresenter: ReadyStatusPresenter, ) {} init(): Promise { @@ -36,6 +39,8 @@ export default class Application { // `chrome.tabs.sendMessage` API with a frame ID. chrome.runtime.connect({ name: "vimmatic-port" }); + this.readyStatusPresenter.setContentReady(); + return Promise.resolve(); } diff --git a/src/content/controllers/ConsoleFrameController.ts b/src/content/controllers/ConsoleFrameController.ts index a37c27ba..d0f37bc0 100644 --- a/src/content/controllers/ConsoleFrameController.ts +++ b/src/content/controllers/ConsoleFrameController.ts @@ -12,6 +12,10 @@ export default class ConsoleFrameController { this.consoleFrameUseCase.unfocus(); } + ready() { + this.consoleFrameUseCase.makeConsoleReady(); + } + resize({ width, height }: { width: number; height: number }) { this.consoleFrameUseCase.resize(width, height); } diff --git a/src/content/di.ts b/src/content/di.ts index 16c22817..e3f261ab 100644 --- a/src/content/di.ts +++ b/src/content/di.ts @@ -18,6 +18,7 @@ import { BackgroundKeyClientImpl } from "./client/BackgroundKeyClient"; import { ModeRepositoryImpl } from "./repositories/ModeRepository"; import { TopFrameClientImpl } from "./client/TopFrameClient"; import { FrameIdRepositoryImpl } from "./repositories/FrameIdRepository"; +import { ReadyStatusPresenterImpl } from "./presenters/ReadyStatusPresenter"; import { Container } from "inversify"; import { newSender as newBackgroundMessageSender } from "./client/BackgroundMessageSender"; import { newSender as newWindowMessageSender } from "./client/WindowMessageSender"; @@ -40,6 +41,7 @@ container.bind("BackgroundKeyClient").to(BackgroundKeyClientImpl); container.bind("ModeRepository").to(ModeRepositoryImpl); container.bind("TopFrameClient").to(TopFrameClientImpl); container.bind("FrameIdRepository").to(FrameIdRepositoryImpl); +container.bind("ReadyStatusPresenter").to(ReadyStatusPresenterImpl); container.bind("SettingClient").to(SettingClientImpl); container.bind("SettingRepository").to(SettingRepositoryImpl); container diff --git a/src/content/messaging/WindowMessageListener.ts b/src/content/messaging/WindowMessageListener.ts index 07e079b4..6ca9e0df 100644 --- a/src/content/messaging/WindowMessageListener.ts +++ b/src/content/messaging/WindowMessageListener.ts @@ -21,6 +21,9 @@ export default class WindowMessageListener { this.receiver .route("console.unfocus") .to(consoleFrameController.unfocus.bind(consoleFrameController)); + this.receiver + .route("console.ready") + .to(consoleFrameController.ready.bind(consoleFrameController)); this.receiver .route("notify.frame.id") .to(topFrameController.saveChildFrame.bind(topFrameController)); diff --git a/src/content/presenters/ReadyStatusPresenter.ts b/src/content/presenters/ReadyStatusPresenter.ts new file mode 100644 index 00000000..0d2f8381 --- /dev/null +++ b/src/content/presenters/ReadyStatusPresenter.ts @@ -0,0 +1,19 @@ +import { injectable } from "inversify"; + +export default interface ReadyStatusPresenter { + setContentReady(): void; + + setConsoleReady(): void; +} + +@injectable() +export class ReadyStatusPresenterImpl { + constructor(private readonly doc: Document = window.document) {} + setContentReady() { + this.doc.head.setAttribute("data-vimmatic-content-status", "ready"); + } + + setConsoleReady() { + this.doc.head.setAttribute("data-vimmatic-console-status", "ready"); + } +} diff --git a/src/content/usecases/ConsoleFrameUseCase.ts b/src/content/usecases/ConsoleFrameUseCase.ts index c3f823b4..b88f1c76 100644 --- a/src/content/usecases/ConsoleFrameUseCase.ts +++ b/src/content/usecases/ConsoleFrameUseCase.ts @@ -1,11 +1,14 @@ import { injectable, inject } from "inversify"; import ConsoleFramePresenter from "../presenters/ConsoleFramePresenter"; +import ReadyStatusPresenter from "../presenters/ReadyStatusPresenter"; @injectable() export default class ConsoleFrameUseCase { constructor( @inject("ConsoleFramePresenter") private readonly consoleFramePresenter: ConsoleFramePresenter, + @inject("ReadyStatusPresenter") + private readonly readyStatusPresenter: ReadyStatusPresenter, ) {} unfocus() { @@ -16,4 +19,8 @@ export default class ConsoleFrameUseCase { resize(width: number, height: number) { this.consoleFramePresenter.resize(width, height); } + + makeConsoleReady() { + this.readyStatusPresenter.setConsoleReady(); + } } diff --git a/src/messaging/schema/window.ts b/src/messaging/schema/window.ts index 9d9841d9..054f1019 100644 --- a/src/messaging/schema/window.ts +++ b/src/messaging/schema/window.ts @@ -3,6 +3,7 @@ import { Simplex } from "../types"; export type Schema = { "console.unfocus": Simplex; + "console.ready": Simplex; "notify.frame.id": Simplex<{ frameId: number }>; }; diff --git a/test/content/presenters/ReadyStatusPresenter.test.ts b/test/content/presenters/ReadyStatusPresenter.test.ts new file mode 100644 index 00000000..62fe4382 --- /dev/null +++ b/test/content/presenters/ReadyStatusPresenter.test.ts @@ -0,0 +1,23 @@ +/** + * @jest-environment jsdom + */ + +import { ReadyStatusPresenterImpl } from "../../../src/content/presenters/ReadyStatusPresenter"; + +describe("ReadyStatusPresenterImpl", () => { + test("sets ready status of the content script", () => { + const sut = new ReadyStatusPresenterImpl(); + sut.setContentReady(); + expect(document.head.getAttribute("data-vimmatic-content-status")).toBe( + "ready", + ); + }); + + test("sets ready status of the console", () => { + const sut = new ReadyStatusPresenterImpl(); + sut.setConsoleReady(); + expect(document.head.getAttribute("data-vimmatic-console-status")).toBe( + "ready", + ); + }); +});