diff --git a/.github/workflows/client-PR.yaml b/.github/workflows/client-PR.yaml new file mode 100644 index 0000000..ddf9300 --- /dev/null +++ b/.github/workflows/client-PR.yaml @@ -0,0 +1,30 @@ +name: 'client-pr' +on: + pull_request: + branches: [main] + paths: + - 'holo-key-manager-js-client/package.json' + - '.github/workflows/client.yaml' + +jobs: + build: + runs-on: ubuntu-latest + + environment: + name: Client + + steps: + - uses: actions/checkout@v4 + - name: Setup Node.js environment + uses: actions/setup-node@v4 + with: + node-version: lts/* + registry-url: 'https://registry.npmjs.org' + - name: Install pnpm + run: npm install -g pnpm + - name: Install dependencies + run: pnpm install --no-frozen-lockfile + working-directory: holo-key-manager-js-client + - name: Run unit tests + run: pnpm test + working-directory: holo-key-manager-js-client diff --git a/.github/workflows/client.yaml b/.github/workflows/client.yaml index 0f4bcc2..0c7626f 100644 --- a/.github/workflows/client.yaml +++ b/.github/workflows/client.yaml @@ -18,7 +18,7 @@ jobs: - name: Setup Node.js environment uses: actions/setup-node@v4 with: - node-version: 20 + node-version: lts/* registry-url: 'https://registry.npmjs.org' - name: Install pnpm run: npm install -g pnpm diff --git a/.github/workflows/extension-PR.yaml b/.github/workflows/extension-PR.yaml index 26169cd..06baebf 100644 --- a/.github/workflows/extension-PR.yaml +++ b/.github/workflows/extension-PR.yaml @@ -17,7 +17,7 @@ jobs: - name: Setup Node.js environment uses: actions/setup-node@v4 with: - node-version: 20 + node-version: lts/* - name: Install pnpm run: npm install -g pnpm diff --git a/.github/workflows/extension.yaml b/.github/workflows/extension.yaml index 6943225..15d851b 100644 --- a/.github/workflows/extension.yaml +++ b/.github/workflows/extension.yaml @@ -17,7 +17,7 @@ jobs: - name: Setup Node.js environment uses: actions/setup-node@v4 with: - node-version: 20 + node-version: lts/* - name: Install pnpm run: npm install -g pnpm diff --git a/package.json b/package.json index a14bfb7..06d66ff 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "eslint-plugin-svelte": "^2.39.3", "globals": "^15.4.0", "husky": "^9.0.11", + "jszip": "^3.10.1", "lint-staged": "^15.2.5", "prettier": "^3.3.2", "puppeteer": "^22.12.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 10626ed..3199100 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -48,6 +48,9 @@ importers: husky: specifier: ^9.0.11 version: 9.0.11 + jszip: + specifier: ^3.10.1 + version: 3.10.1 lint-staged: specifier: ^15.2.5 version: 15.2.7 diff --git a/tests/helpers.ts b/tests/helpers.ts index a56f4f2..6f82163 100644 --- a/tests/helpers.ts +++ b/tests/helpers.ts @@ -1,3 +1,4 @@ +import { access, constants } from 'fs'; import { Browser, ElementHandle, launch, Page } from 'puppeteer'; export const launchBrowserWithExtension = async (extensionPath: string): Promise => { @@ -13,21 +14,43 @@ export const openExtensionPage = async (browser: Browser, extensionId: string): return page; }; +const waitForNewPage = (browser: Browser): Promise => + new Promise((resolve, reject) => + browser.once('targetcreated', async (target) => { + const page = await target.page(); + page ? resolve(page) : reject(new Error('Failed to create new page')); + }) + ); + export const clickButtonAndWaitForNewPage = async ( browser: Browser, - button: ElementHandle + button: ElementHandle, + downloadPath: string ): Promise => { await button?.click(); - - const newPagePromise = new Promise((resolve) => - browser.once('targetcreated', async (target) => resolve(await target.page())) - ); - - const newPage = await newPagePromise; - if (!newPage) { - throw new Error('Failed to create new page'); - } + const newPage = await waitForNewPage(browser); await newPage.waitForNavigation(); + const session = await newPage.createCDPSession(); + await session.send('Page.setDownloadBehavior', { + behavior: 'allow', + downloadPath: downloadPath + }); + return newPage; }; + +export const fileExists = (filePath: string): Promise => { + return new Promise((resolve) => { + const checkFile = () => { + access(filePath, constants.F_OK, (err) => { + if (!err) { + resolve(true); + } else { + setTimeout(checkFile, 100); + } + }); + }; + checkFile(); + }); +}; diff --git a/tests/index.test.ts b/tests/index.test.ts index ab18847..a0b6f71 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -1,10 +1,14 @@ import dotenv from 'dotenv'; -import path from 'path'; +import { rmdirSync } from 'fs'; +import fs from 'fs/promises'; +import JSZip from 'jszip'; +import { join, resolve } from 'path'; import type { Browser, Page } from 'puppeteer'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { clickButtonAndWaitForNewPage, + fileExists, launchBrowserWithExtension, openExtensionPage } from './helpers'; @@ -13,6 +17,8 @@ dotenv.config(); const EXTENSION_ID = process.env.CHROME_ID; +const downloadPath = resolve('./downloads'); + let browser: Browser; let page: Page; @@ -20,12 +26,13 @@ beforeAll(async () => { if (!EXTENSION_ID) { throw new Error('EXTENSION_ID is not set'); } - const extensionPath = path.resolve('holo-key-manager-extension', 'build'); + const extensionPath = resolve('holo-key-manager-extension', 'build'); browser = await launchBrowserWithExtension(extensionPath); page = await openExtensionPage(browser, EXTENSION_ID); }); afterAll(async () => { + rmdirSync(downloadPath, { recursive: true }); await browser?.close(); }); @@ -38,7 +45,8 @@ describe('Extension E2E Tests', () => { if (!setupButton) { throw new Error('Button not found'); } - const setupPage = await clickButtonAndWaitForNewPage(browser, setupButton); + + const setupPage = await clickButtonAndWaitForNewPage(browser, setupButton, downloadPath); const setupPageContent = await setupPage.content(); @@ -81,5 +89,90 @@ describe('Extension E2E Tests', () => { const enterPassphrasePageContent = await setupPage.content(); expect(enterPassphrasePageContent).toContain('Enter Passphrase'); + + const enterPassphraseInput = await setupPage.waitForSelector( + 'textarea[placeholder="Enter Passphrase"]' + ); + if (!enterPassphraseInput) { + throw new Error('Password inputs not found'); + } + + await enterPassphraseInput.type('passphrase passphrase'); + + const setPassphraseButton = await setupPage.waitForSelector( + 'button::-p-text("Set passphrase")' + ); + + if (!setPassphraseButton) { + throw new Error('Button not found'); + } + + await setPassphraseButton.click(); + + const confirmPassphrasePageContent = await setupPage.content(); + + expect(confirmPassphrasePageContent).toContain('Confirm Passphrase'); + + const confirmPassphraseInput = await setupPage.waitForSelector( + 'textarea[placeholder="Confirm Passphrase"]' + ); + + if (!confirmPassphraseInput) { + throw new Error('Password inputs not found'); + } + + await confirmPassphraseInput.type('passphrase passphrase'); + + const confirmPassphraseButton = await setupPage.waitForSelector('button::-p-text("Next")'); + + if (!confirmPassphraseButton) { + throw new Error('Button not found'); + } + + await Promise.all([setupPage.waitForNavigation(), confirmPassphraseButton.click()]); + + const generateSeedPageContent = await setupPage.content(); + + expect(generateSeedPageContent).toContain('Generate seed and key files'); + + const generateButton = await setupPage.waitForSelector('button::-p-text("Generate")'); + + if (!generateButton) { + throw new Error('Button not found'); + } + + await Promise.all([setupPage.waitForNavigation(), generateButton.click()]); + + const saveSeedPageContent = await setupPage.content(); + + expect(saveSeedPageContent).toContain('Save seed and key files'); + + const exportButton = await setupPage.waitForSelector('button::-p-text("Export")'); + + if (!exportButton) { + throw new Error('Button not found'); + } + + exportButton.click(); + + const keysFilePath = join(downloadPath, 'keys.zip'); + + const keysFileExists = await fileExists(keysFilePath); + + expect(keysFileExists).toBe(true); + + const zipContent = await fs.readFile(keysFilePath); + const zip = await JSZip.loadAsync(zipContent); + + const expectedFiles = ['device.txt', 'master.txt', 'revocation.txt']; + const actualFiles = Object.keys(zip.files); + + expectedFiles.forEach((file) => { + expect(actualFiles).toContain(file); + }); + + const setupCompletePageContent = await setupPage.content(); + + expect(setupCompletePageContent).toContain('Setup Complete'); }, 10000); });