diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index 9b72f99..0000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: Cypress Tests - -on: push - -jobs: - cypress-run: - runs-on: ubuntu-22.04 - steps: - - name: Checkout - uses: actions/checkout@v4 - # Install NPM dependencies, cache them correctly - # and run all Cypress tests - - name: Cypress run - uses: cypress-io/github-action@v6 - with: - build: npm run build - start: npm start \ No newline at end of file diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 0000000..90b6b70 --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,27 @@ +name: Playwright Tests +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 18 + - name: Install dependencies + run: npm ci + - name: Install Playwright Browsers + run: npx playwright install --with-deps + - name: Run Playwright tests + run: npx playwright test + - uses: actions/upload-artifact@v3 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/.gitignore b/.gitignore index ed6813b..7c8f602 100644 --- a/.gitignore +++ b/.gitignore @@ -38,4 +38,10 @@ next-env.d.ts #extension /extensionReqs/extension.* /extension -extension.crx \ No newline at end of file +extension.crx +**/*.zip + +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/components/AliasedEmails.tsx b/components/AliasedEmails.tsx index 944729e..de5f36e 100644 --- a/components/AliasedEmails.tsx +++ b/components/AliasedEmails.tsx @@ -27,18 +27,15 @@ import { useEffect, useState } from "react" import { useAtom } from "jotai" import { colorSelector } from "@/utils/colorSelector" import { localCopyHistoryAtom } from "./global/CopyHistory" +import { AliasType } from "@/components/types" type Props = { extension?: boolean } -type Alias = { - label: string - value: string -} export default function InputCreator({ extension }: Props) { const [email, setEmail] = useLocalStorage({ key: "email", defaultValue: "" }) const [aliases, setAliases] = useLocalStorage({ key: "aliases", - defaultValue: [] as Alias[], + defaultValue: [] as AliasType[], }) const [selectedAlias, setSelectedAlias] = useLocalStorage({ key: "selectedAlias", diff --git a/components/global/CopyHistory.tsx b/components/global/CopyHistory.tsx index 6c7a2d4..3f7c57d 100644 --- a/components/global/CopyHistory.tsx +++ b/components/global/CopyHistory.tsx @@ -15,6 +15,7 @@ import { import { IconCopy } from "@tabler/icons-react" import { colorSelector } from "@/utils/colorSelector" import { useRouter } from "next/router" +import { CopyHistoryType } from "@/components/types" type Props = { type?: "email" | "text" @@ -23,20 +24,9 @@ type Props = { scrollThreshold: number } -export type CopyHistory = { - id: number - type: string - value: string - timestamp?: number -} - -interface GroupedCopyHistory { - [dateKey: number]: CopyHistory[] -} - export const localCopyHistoryAtom = atomWithStorage( "copyHistory", - [] as CopyHistory[] + [] as CopyHistoryType[] ) export default function CopyHistory({ type, tooltip, scrollThreshold }: Props) { @@ -115,7 +105,7 @@ export default function CopyHistory({ type, tooltip, scrollThreshold }: Props) { historyItem, ...rest }: { - historyItem: CopyHistory + historyItem: CopyHistoryType }) => { return ( @@ -176,8 +166,8 @@ export default function CopyHistory({ type, tooltip, scrollThreshold }: Props) { } } - const groupedData: { [key: string]: CopyHistory[] } = copyHistory.reduce( - (acc: { [key: string]: CopyHistory[] }, item) => { + const groupedData: { [key: string]: CopyHistoryType[] } = copyHistory.reduce( + (acc: { [key: string]: CopyHistoryType[] }, item) => { let itemTimestamp = item.timestamp if (itemTimestamp == undefined || itemTimestamp == null) { itemTimestamp = new Date(946800000000).getTime() @@ -192,7 +182,7 @@ export default function CopyHistory({ type, tooltip, scrollThreshold }: Props) { } return groupedData }, - {} as { [key: string]: CopyHistory[] } + {} as { [key: string]: CopyHistoryType[] } ) const dateKeys = Object.keys(groupedData) @@ -260,7 +250,7 @@ export default function CopyHistory({ type, tooltip, scrollThreshold }: Props) { {groupedData[dates] .sort() .reverse() - .map((historyItem: CopyHistory) => ( + .map((historyItem: CopyHistoryType) => ( { - const email = "idxqamv@gmail.com" - const aliasedEmail = "idxqamv+SugarFire@gmail.com" - - it("stores expected data", () => { - cy.visit("/") - cy.get('input[type="email"]').type(email) - cy.get('input[type="alias"]').type("Sugar Fire{enter}") - cy.get('[data-cy="timestampEnabled"]').uncheck({ force: true }) - cy.get("#copyEmail").invoke("text").should("eq", aliasedEmail) - cy.get("#copyEmail").click() - cy.getAllLocalStorage().then((result) => { - expect(result["http://localhost:3000"]).to.contain.keys( - "email", - "aliases", - "copyHistory", - "selectedAlias", - "timestampEnabled" - ) - }) - cy.get(`[data-cy="${aliasedEmail}"]`).should("exist") - }) -}) diff --git a/cypress/fixtures/example.json b/cypress/fixtures/example.json deleted file mode 100644 index 02e4254..0000000 --- a/cypress/fixtures/example.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "Using fixtures to represent data", - "email": "hello@cypress.io", - "body": "Fixtures are a great way to mock data for responses to routes" -} diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts deleted file mode 100644 index 698b01a..0000000 --- a/cypress/support/commands.ts +++ /dev/null @@ -1,37 +0,0 @@ -/// -// *********************************************** -// This example commands.ts shows you how to -// create various custom commands and overwrite -// existing commands. -// -// For more comprehensive examples of custom -// commands please read more here: -// https://on.cypress.io/custom-commands -// *********************************************** -// -// -// -- This is a parent command -- -// Cypress.Commands.add('login', (email, password) => { ... }) -// -// -// -- This is a child command -- -// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) -// -// -// -- This is a dual command -- -// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) -// -// -// -- This will overwrite an existing command -- -// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) -// -// declare global { -// namespace Cypress { -// interface Chainable { -// login(email: string, password: string): Chainable -// drag(subject: string, options?: Partial): Chainable -// dismiss(subject: string, options?: Partial): Chainable -// visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable -// } -// } -// } \ No newline at end of file diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts deleted file mode 100644 index f80f74f..0000000 --- a/cypress/support/e2e.ts +++ /dev/null @@ -1,20 +0,0 @@ -// *********************************************************** -// This example support/e2e.ts is processed and -// loaded automatically before your test files. -// -// This is a great place to put global configuration and -// behavior that modifies Cypress. -// -// You can change the location of this file or turn off -// automatically serving support files with the -// 'supportFile' configuration option. -// -// You can read more here: -// https://on.cypress.io/configuration -// *********************************************************** - -// Import commands.js using ES2015 syntax: -import './commands' - -// Alternatively you can use CommonJS syntax: -// require('./commands') \ No newline at end of file diff --git a/extension.zip b/extension.zip deleted file mode 100644 index 5a5bcb1..0000000 Binary files a/extension.zip and /dev/null differ diff --git a/package-lock.json b/package-lock.json index da453fe..11aa78a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "quick-lorem", - "version": "1.3.0", + "version": "1.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "quick-lorem", - "version": "1.3.0", + "version": "1.3.1", "license": "GPL", "dependencies": { "@emotion/react": "^11.10.6", @@ -30,6 +30,7 @@ }, "devDependencies": { "@faker-js/faker": "^8.1.0", + "@playwright/test": "^1.40.1", "@swc-jotai/react-refresh": "^0.0.8", "cypress": "^13.6.0" } @@ -834,6 +835,21 @@ "node": ">= 8" } }, + "node_modules/@playwright/test": { + "version": "1.40.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.40.1.tgz", + "integrity": "sha512-EaaawMTOeEItCRvfmkI9v6rBkF1svM8wjl/YPRrg2N2Wmp+4qJYkWtJsbew1szfKKDm6fPLy4YAanBhIlf9dWw==", + "dev": true, + "dependencies": { + "playwright": "1.40.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/@radix-ui/number": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.0.0.tgz", @@ -3283,6 +3299,20 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -5025,6 +5055,36 @@ "node": ">=0.10.0" } }, + "node_modules/playwright": { + "version": "1.40.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.40.1.tgz", + "integrity": "sha512-2eHI7IioIpQ0bS1Ovg/HszsN/XKNwEG1kbzSDDmADpclKc7CyqkHw7Mg2JCz/bbCxg25QUPcjksoMW7JcIFQmw==", + "dev": true, + "dependencies": { + "playwright-core": "1.40.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.40.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.40.1.tgz", + "integrity": "sha512-+hkOycxPiV534c4HhpfX6yrlawqVUzITRKwHAmYfmsVreltEl6fAZJ3DPfLMOODw0H3s1Itd6MDCWmP1fl/QvQ==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/postcss": { "version": "8.4.14", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz", diff --git a/package.json b/package.json index f5ff4d2..0e08794 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "build": "next build", "start": "next start", "lint": "next lint", - "test": "cypress run", + "test": "playwright test", + "test-dev": "playwright test --ui", "vercel-build": "next build", "build:extension": "extensionReqs/build.sh" }, @@ -50,6 +51,7 @@ }, "devDependencies": { "@faker-js/faker": "^8.1.0", + "@playwright/test": "^1.40.1", "@swc-jotai/react-refresh": "^0.0.8", "cypress": "^13.6.0" } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..57cdb26 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,79 @@ +import { defineConfig, devices } from "@playwright/test" + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: "./tests", + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: "html", + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: "http://localhost:3000", + + testIdAttribute: "data-cy", + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: "on-first-retry", + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + + { + name: "firefox", + use: { ...devices["Desktop Firefox"] }, + }, + + { + name: "webkit", + use: { ...devices["Desktop Safari"] }, + }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: "npm run dev", + url: "http://localhost:3000", + reuseExistingServer: !process.env.CI, + }, +}) diff --git a/tests/aliasedEmail.spec.ts b/tests/aliasedEmail.spec.ts new file mode 100644 index 0000000..50329e9 --- /dev/null +++ b/tests/aliasedEmail.spec.ts @@ -0,0 +1,71 @@ +import { AliasType, CopyHistoryType, TotalLocalStorage } from "@/components/types" +import test, { expect } from "@playwright/test" + +test.describe("aliased emails", () => { + const email = "idxqamv@gmail.com" + const aliasedEmail = "idxqamv+SugarFire@gmail.com" + + test("stores expected data", async ({ page }) => { + await page.goto("/") + await page.fill('input[type="email"]', email) + await page.fill('input[type="alias"]', "Sugar Fire") + await page.press('input[type="alias"]', "Enter") + await page.getByText("Time").click() + await expect(page.getByTestId("timestampEnabled")).not.toBeChecked() + expect(await page.textContent("#copyEmail")).toEqual(aliasedEmail) + await page.click("#copyEmail") + const savedLocalStorage = (await page.context().storageState()).origins[0] + expect(savedLocalStorage.localStorage.length).toBe(5) + const expectedStorage: TotalLocalStorage = [ + { + name: "selectedAlias", + value: "SugarFire", + }, + { + name: "email", + value: email, + }, + { + name: "copyHistory", + value: [{ id: 0, type: "email", value: aliasedEmail }], + }, + { + name: "aliases", + value: [{ label: "SugarFire", value: "SugarFire" }], + }, + { + name: "timestampEnabled", + value: false, + }, + ] + + const expectedStorageMap = Object.fromEntries( + expectedStorage.map((item) => [item.name, item.value]) + ) + const savedLocalStorageMap = Object.fromEntries( + savedLocalStorage.localStorage.map((item) => [ + item.name, + JSON.parse(item.value), + ]) + ) + + for (const key in expectedStorageMap) { + if (key === "copyHistory") { + //@ts-ignore + const expectedCopyHistory: CopyHistoryType = expectedStorageMap[key] + const storedCopyHistory: CopyHistoryType = savedLocalStorageMap[key] + expect(storedCopyHistory).toMatchObject(expectedCopyHistory) + } else if (key === "aliases") { + //@ts-ignore + const expectedAliases: AliasType[] = expectedStorageMap[key] + const storedAliases: AliasType[] = savedLocalStorageMap[key] + expect(storedAliases).toMatchObject(expectedAliases) + } else { + expect(JSON.stringify(savedLocalStorageMap[key])).toMatch( + JSON.stringify(expectedStorageMap[key]) + ) + } + } + await expect(page.getByTestId(aliasedEmail)).toBeVisible() + }) +})