From 210c3ba505bc19d5694407da56f839a02b986d0b Mon Sep 17 00:00:00 2001 From: Rob Gordon Date: Thu, 27 Jul 2023 13:37:33 -0400 Subject: [PATCH 1/2] Log in to Access Local Charts (#563) * Scaffold new auth page * Slow React 18 Migration * Tests passing; Update mostly complete * Revert to unnecessary supbase wrapper * Load all layouts always * Fix line highlight bug * Working on handling login error * Align TS checking and eslint * Adjust supbase init * Two unnecessary logs for findng * Remove Logs * Switch to await * Lots of error wrappers * Back to the way it was * RAF; resolve session * Cleanup error message * Remove stripe validation before login * Add auth wall to new page * Fullscreen disables banners * Remove comment * Remove log * Move e2e test to auth tests * Update playwright version; Begin Re-org Tests * Add email/pass login * Add Sign In vs Sign Up password option * Added Reset Password Flow * Fix many e2e tests * Update CI env vars * Fix more e2e tests * Disable Pro tests temporarily; fix template * Skip final failing test * Revert lingui/cli version * Add translations * Improve selector in e2e test * Scaffold new auth page * Slow React 18 Migration * Tests passing; Update mostly complete * Added Reset Password Flow * Add missing translations to Login Page * Add callout to Pricing Page * Add translations * Make subscription optional at customer-info * Un-skip pro tests * Fix Lockfile * Add Privacy Policy links back * Fix supabase client * Update Log In Change * Update Supabase to Fix OAuth Login * Fix MightLoseWarning bug * Add default clone chart name --- .github/workflows/e2e.yml | 6 +- .husky/pre-commit | 2 +- api/customer-info.ts | 16 +- api/get-signup-client-secret.ts | 6 +- api/package.json | 2 +- app/.eslintrc.cjs | 26 + app/.eslintrc.js | 70 - app/e2e/auth.spec.ts | 278 - app/e2e/authenticated.spec.ts | 194 + app/e2e/graph-floating-menu.spec.ts | 1 - app/e2e/heads-up.spec.ts | 13 +- app/e2e/monthly.spec.ts | 100 - app/e2e/open-syntax-reference.spec.ts | 3 - app/e2e/pro.spec.ts | 277 + app/e2e/share-links.spec.ts | 63 - app/e2e/unauth.spec.ts | 552 +- app/e2e/utils.ts | 13 +- app/package.json | 77 +- app/playwright.config.ts | 2 +- app/scripts/autotranslations.mjs | 21 +- app/src/components/AppContext.tsx | 50 +- app/src/components/AuthWall.tsx | 69 + app/src/components/CloneButton.tsx | 19 +- app/src/components/ColorMode.tsx | 22 +- app/src/components/EditorOptions.tsx | 6 +- app/src/components/EditorWrapper.tsx | 1 + app/src/components/Feedback.test.tsx | 26 +- app/src/components/Feedback.tsx | 10 +- app/src/components/Graph.tsx | 18 - app/src/components/GraphContextMenu.tsx | 16 +- app/src/components/GraphFloatingMenu.tsx | 20 +- app/src/components/Header.tsx | 92 +- .../ImportDataUnauthenticatedDialog.tsx | 6 +- app/src/components/Layout.tsx | 53 +- .../components/MightLoseSponsorTrigger.tsx | 6 +- app/src/components/MightLoseWarning.tsx | 13 +- app/src/components/OnlyInEnglish.tsx | 2 +- app/src/components/PaymentStepper.tsx | 55 +- app/src/components/RenameButton.tsx | 14 +- app/src/components/Router.tsx | 146 +- app/src/components/Settings.tsx | 22 +- app/src/components/ShareDialog.tsx | 12 +- app/src/components/SvgProOnlyPopover.tsx | 6 +- app/src/components/Tabs/EditLayoutTab.tsx | 14 +- app/src/components/Tabs/EditorTabList.tsx | 6 +- app/src/components/TextEditor.tsx | 5 +- app/src/components/Warning.tsx | 2 +- app/src/components/WelcomeMessage.tsx | 2 +- app/src/index.tsx | 14 +- app/src/lib/cytoscape.ts | 18 +- app/src/lib/helpers.ts | 18 +- app/src/lib/hooks.ts | 50 +- app/src/lib/queries.ts | 42 +- app/src/lib/useGraphStore.ts | 2 - app/src/locales/de/messages.js | 462 +- app/src/locales/de/messages.po | 382 +- app/src/locales/en/messages.js | 456 +- app/src/locales/en/messages.po | 382 +- app/src/locales/fr/messages.js | 462 +- app/src/locales/fr/messages.po | 382 +- app/src/locales/hi/messages.js | 458 +- app/src/locales/hi/messages.po | 382 +- app/src/locales/ko/messages.js | 450 +- app/src/locales/ko/messages.po | 382 +- app/src/locales/pt-br/messages.js | 459 +- app/src/locales/pt-br/messages.po | 382 +- app/src/locales/zh/messages.js | 433 +- app/src/locales/zh/messages.po | 382 +- app/src/pages/Account.tsx | 272 +- app/src/pages/Charts.tsx | 35 +- app/src/pages/Edit.tsx | 12 +- app/src/pages/EditHosted.tsx | 18 +- app/src/pages/ForgotPassword.tsx | 78 + app/src/pages/LogIn.tsx | 336 +- app/src/pages/New.tsx | 14 +- app/src/pages/Public.tsx | 15 +- app/src/pages/ReadOnly.tsx | 29 +- app/src/pages/ResetPassword.tsx | 27 + app/src/pages/post/Post.tsx | 16 +- app/src/test-utils.tsx | 20 +- app/src/types/database.types.ts | 63 +- app/src/ui/Shared.tsx | 58 +- pnpm-lock.yaml | 5936 ++++++++++------- 83 files changed, 6963 insertions(+), 8399 deletions(-) create mode 100644 app/.eslintrc.cjs delete mode 100644 app/.eslintrc.js delete mode 100644 app/e2e/auth.spec.ts create mode 100644 app/e2e/authenticated.spec.ts delete mode 100644 app/e2e/monthly.spec.ts create mode 100644 app/e2e/pro.spec.ts delete mode 100644 app/e2e/share-links.spec.ts create mode 100644 app/src/components/AuthWall.tsx create mode 100644 app/src/pages/ForgotPassword.tsx create mode 100644 app/src/pages/ResetPassword.tsx diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index d805720b..294286e0 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -31,8 +31,8 @@ jobs: touch app/.env echo RAPID_API_KEY=${{ secrets.RAPID_API_KEY }} >> app/.env echo STRIPE_KEY_TEST_ENV=${{ secrets.STRIPE_KEY_TEST_ENV }} >> app/.env - echo SUPABASE_TEST_URL=${{ secrets.SUPABASE_TEST_URL }} >> app/.env - echo SUPABASE_TEST_ANON_KEY=${{ secrets.SUPABASE_TEST_ANON_KEY }} >> app/.env + echo TESTING_EMAIL=${{ secrets.TESTING_EMAIL }} >> app/.env + echo TESTING_PASS=${{ secrets.TESTING_PASS }} >> app/.env - name: Setup Node uses: actions/setup-node@v3 with: @@ -42,7 +42,7 @@ jobs: version: 8 - name: Install Playwright run: | - pnpm add -g playwright@1.29.0 + pnpm add -g playwright@1.36.2 playwright install - name: Install Deps run: pnpm install diff --git a/.husky/pre-commit b/.husky/pre-commit index fab6428a..c7874ec1 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,4 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" -pnpm lint-staged +pnpm lint-staged && pnpm -F app typecheck diff --git a/api/customer-info.ts b/api/customer-info.ts index e0441126..af3a6a57 100644 --- a/api/customer-info.ts +++ b/api/customer-info.ts @@ -16,15 +16,15 @@ export default async function customerInfo( status: "all", }); - const subscription = subscriptions.length ? subscriptions[0] : undefined; - const priceId = subscription?.items.data[0].plan.id; - - if (!subscription || !priceId) throw new Error("No Subscription Found"); + // find the first valid subscription + const validSubscriptions = subscriptions.filter((subscription) => { + const priceId = subscription.items.data[0].plan.id; + return validStripePrices.includes(priceId); + }); - // make sure priceId is valid - if (!validStripePrices.includes(priceId)) { - throw new Error("Invalid Subscription"); - } + const subscription = validSubscriptions.length + ? validSubscriptions[0] + : undefined; res.json({ customerId: customer.id, subscription }); } catch (error) { diff --git a/api/get-signup-client-secret.ts b/api/get-signup-client-secret.ts index 5b96eec0..6cc30df2 100644 --- a/api/get-signup-client-secret.ts +++ b/api/get-signup-client-secret.ts @@ -36,8 +36,10 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { // If they already have a subscription in validStripePrices, return error if ( - subscriptions.some((sub) => - validStripePrices.includes(sub.items.data[0].price.id) + subscriptions.some( + (sub) => + validStripePrices.includes(sub.items.data[0].price.id) && + sub.status !== "incomplete" ) ) { return res.status(402).json({ diff --git a/api/package.json b/api/package.json index 2d70abfa..e242dcb9 100644 --- a/api/package.json +++ b/api/package.json @@ -36,6 +36,6 @@ "@vercel/node": "^2.8.15", "eslint": "^8.3.0", "jest": "^29.4.3", - "typescript": "^4.8.4" + "typescript": "^5.1.6" } } diff --git a/app/.eslintrc.cjs b/app/.eslintrc.cjs new file mode 100644 index 00000000..8d0242fa --- /dev/null +++ b/app/.eslintrc.cjs @@ -0,0 +1,26 @@ +module.exports = { + root: true, + env: { browser: true, es2020: true }, + extends: [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:react-hooks/recommended", + "plugin:jsx-a11y/strict", + ], + ignorePatterns: ["dist", ".eslintrc.cjs"], + parser: "@typescript-eslint/parser", + plugins: ["jsx-a11y", "simple-import-sort", "react-refresh"], + rules: { + "@typescript-eslint/no-unused-vars": [ + "warn", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + }, + ], + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/ban-ts-comment": "off", + "no-debugger": "warn", + "prefer-const": "off", + }, +}; diff --git a/app/.eslintrc.js b/app/.eslintrc.js deleted file mode 100644 index d9db4138..00000000 --- a/app/.eslintrc.js +++ /dev/null @@ -1,70 +0,0 @@ -module.exports = { - env: { - browser: true, - es6: true, - }, - settings: { - react: { - version: "detect", - }, - }, - extends: [ - "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended", - "plugin:react/recommended", - "plugin:react-hooks/recommended", - "plugin:prettier/recommended", - "plugin:jsx-a11y/strict", - ], - parser: "@typescript-eslint/parser", - parserOptions: { - ecmaFeatures: { - jsx: true, - }, - ecmaVersion: 2018, - sourceType: "module", - }, - plugins: ["react", "jsx-a11y", "@typescript-eslint", "simple-import-sort"], - rules: { - "@typescript-eslint/no-unused-vars": [ - "warn", - { - argsIgnorePattern: "^_", - varsIgnorePattern: "^_", - }, - ], - "react-hooks/exhaustive-deps": "warn", - "no-var": "error", - "no-debugger": "warn", - "brace-style": "error", - "prefer-template": "error", - radix: "error", - "space-before-blocks": "error", - "import/prefer-default-export": "off", - "react/react-in-jsx-scope": "off", - "@typescript-eslint/explicit-module-boundary-types": "off", - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/ban-ts-comment": "off", - "@typescript-eslint/ban-types": "off", - "react/prop-types": 0, - "@typescript-eslint/no-empty-interface": 0, - "simple-import-sort/imports": "error", - }, - ignorePatterns: ["api/**/*.js"], - overrides: [ - { - files: [ - "**/*.test.js", - "**/*.test.jsx", - "**/*.test.tsx", - "**/*.spec.js", - "**/*.spec.jsx", - "**/*.spec.tsx", - ], - env: { - jest: true, - }, - }, - ], -}; diff --git a/app/e2e/auth.spec.ts b/app/e2e/auth.spec.ts deleted file mode 100644 index 3cd81fc2..00000000 --- a/app/e2e/auth.spec.ts +++ /dev/null @@ -1,278 +0,0 @@ -import { expect, Page, test } from "@playwright/test"; -import jsdom from "jsdom"; -import path from "path"; - -import { openExportDialog } from "./openExportDialog"; -import { - BASE_URL, - deleteCustomerByEmail, - getTempEmail, - getTempEmailMessage, -} from "./utils"; - -test.describe.configure({ - mode: "serial", -}); - -let email = ""; - -let page: Page; - -test.beforeAll(async ({ browser }) => { - page = await browser.newPage(); - await page.goto(BASE_URL); -}); - -test.afterAll(async () => { - await page.close(); -}); - -test.describe("Authenticated Tasks", () => { - test("Sign Up", async () => { - // set timeout - test.setTimeout(240000); - - await page.getByRole("link", { name: "Pricing" }).click(); - await expect(page).toHaveURL(`${BASE_URL}/pricing`); - - await page.getByTestId("yearly-plan-button").click(); - await page.getByRole("button", { name: "Continue" }).click(); - - email = await getTempEmail(); - await page.getByTestId("email-input").fill(email); - await page.getByRole("button", { name: "Continue" }).click(); - - const iframe = page.frameLocator( - 'internal:attr=[title="Secure payment input frame"i]' - ); - - await iframe - .getByPlaceholder("1234 1234 1234 1234") - .fill("4242 4242 4242 4242"); - await iframe.getByPlaceholder("MM / YY").fill("05 / 50"); - - await iframe.getByPlaceholder("CVC").fill("222"); - await iframe.getByRole("combobox", { name: "Country" }).selectOption("US"); - await iframe.getByPlaceholder("12345").fill("12345"); - - await page.getByRole("button", { name: "Sign Up" }).click(); - - // expect the url to start with "/l" - await expect(page).toHaveURL(new RegExp(`${BASE_URL}/l`)); - - // expect sign up success to be on the screen - await page - .getByRole("heading", { name: "Welcome to Flowchart Fun Pro!" }) - .click(); - - /* Part 2: Get Auth Email */ - await page.getByLabel("Email").fill(email); - await page.getByRole("button", { name: "Submit" }).click(); - await expect( - page.getByText( - "Check your email for a link to log in. You can close this window." - ) - ).toBeVisible({ timeout: 60000 }); - - // wait for email - let emails = [], - i = 0; - while (emails.length === 0 && i < 20) { - // Wait 5 seconds - await new Promise((resolve) => setTimeout(resolve, 10000)); - // Check for email - const response = await getTempEmailMessage(email); - if (!("error" in response)) { - emails = response; - } - i++; - } - - expect(emails.length).toBeGreaterThanOrEqual(1); - const signInEmailIndex = emails.findIndex((email: { mail_html: string }) => - /supabase.co\/auth\/v1\/verify/.test(email.mail_html) - ); - - expect(signInEmailIndex).toBeGreaterThanOrEqual(0); - - // apply this to a new document to get link and then load link and confirm the Account link present - const html = emails[signInEmailIndex].mail_html; - const { window } = new jsdom.JSDOM(html); - const { document } = window; - const link = document.querySelector("a"); - expect(link).toBeTruthy(); - expect(link?.href).toBeTruthy(); - await page.goto(link?.href as string); - - // expect link with "Account" to be present - await expect(page.getByText("Account")).toBeVisible({ timeout: 10 * 1000 }); - }); - - test("can publish chart", async () => { - // page - await page.getByRole("link", { name: "New" }).click(); - - // Make a new hosted chart - await page.getByPlaceholder("Untitled").fill("my new chart"); - await page - .getByRole("radio", { - name: "Persistent Stored in the cloud Accessible from any device", - }) - .click(); - - await page.getByRole("button", { name: "Create" }).click(); - - // expect url to be regex BASE_URL + /u/\d+ - await expect(page).toHaveURL(new RegExp(`${BASE_URL}/u/\\d+`)); - - await page.getByRole("button", { name: "Export" }).click(); - await page.getByLabel("Make publicly accessible").check(); - - // read the value from the textbox with the name 'Copy Public Link' - const publicLink = page.getByRole("textbox", { - name: "Copy Public Link", - }); - - const publicLinkValue = await publicLink.getAttribute("value"); - - if (!publicLinkValue) throw new Error("Public link value is empty"); - - // navigate to public url - await page.goto(publicLinkValue); - - // expect url to be regex BASE_URL + /p/\w+-\w+-\w+ - await expect(page).toHaveURL(new RegExp(`${BASE_URL}/p/\\w+-\\w+-\\w+`)); - - // expect Clone button to be present - await expect(page.getByRole("button", { name: "Clone" })).toBeVisible(); - }); - - test("Download SVG", async () => { - // Create a blank local chart - await page.goto(`${BASE_URL}/download-svg`); - await openExportDialog(page); - // Click [aria-label="Download SVG"] - const [download] = await Promise.all([ - page.waitForEvent("download"), - page.locator('[aria-label="Download SVG"]').click(), - ]); - - expect(download.suggestedFilename()).toBe("download-svg.svg"); - }); - - test("can convert chart to hosted from Might Lose Trigger", async () => { - // Create a blank local chart - await page.goto(`${BASE_URL}/my-new-chart`); - - // Hover [data-testid="might-lose-sponsor-trigger"] then wait for the button to appear - await page.getByTestId("might-lose-sponsor-trigger").click(); - - // Make sure the input with the label Convert to hosted chart? is checked - await page.getByTestId("convert-to-hosted").click(); - - // Add a character to make name different - await page.getByRole("textbox").click(); - await page.getByRole("textbox").fill("my-new-chart-"); - - // Rename - await page.getByRole("button", { name: "Rename" }).click(); - - // expect "/u/" to be in the url - await expect(page).toHaveURL(new RegExp(`${BASE_URL}/u/\\d+`)); - }); - - test("can create chart from prompt by instruction", async () => { - await page.getByRole("link", { name: "New" }).click(); - await page.getByRole("radio", { name: "Prompt" }).click(); - await page.getByTestId("instruct").click(); - await page.getByTestId("prompt-entry-textarea").click(); - await page - .getByTestId("prompt-entry-textarea") - .fill("the stages of the water cycle"); - await page.getByRole("button", { name: "Create" }).click(); - // expect url to be regex BASE_URL + /u/\d+ - await expect(page).toHaveURL(new RegExp(`${BASE_URL}/u/\\d+`), { - timeout: 12000, - }); - }); - - test("can create chart from prompt by extraction", async () => { - await page.getByRole("link", { name: "New" }).click(); - await page.getByRole("radio", { name: "Prompt" }).click(); - await page.getByTestId("extract").click(); - await page.getByTestId("prompt-entry-textarea").click(); - await page - .getByTestId("prompt-entry-textarea") - .fill("a is greater than b but less than a"); - await page.getByRole("button", { name: "Create" }).click(); - // expect url to be regex BASE_URL + /u/\d+ - await expect(page).toHaveURL(new RegExp(`${BASE_URL}/u/\\d+`), { - timeout: 12000, - }); - }); - - test("can create chart from imported data", async () => { - try { - await page.getByRole("button", { name: "Import Data" }).click(); - - const filePath = path.join( - __dirname, - "../../api/data/fixtures/example-visio-process.csv" - ); - - // make the file input visible - // execute this code on the page: document.querySelector("[data-testid=import-data-file-uploader]").style.display = "block" - await page.evaluate(() => { - const fileInput = document.querySelector( - "[data-testid=import-data-file-uploader]" - ) as HTMLElement; - fileInput.style.display = "block"; - }); - - // click the text "Drag and drop a CSV file here, or click to select a file" - // await page.getByText("Drag and drop a CSV file here").click(); - // await page.getByTestId("import-data-file-uploader").click(); - await page - .getByTestId("import-data-file-uploader") - .setInputFiles(filePath); - - await page.getByTestId("node-label-select").press("Enter"); - await page - .getByRole("option", { name: "Process Step Description" }) - .press("Enter"); - - await page.getByTestId("edges-in-source-node-row").click(); - - await page.getByTestId("target-column-select").press("Enter"); - await page.getByRole("option", { name: "Next Step ID" }).press("Enter"); - - await page.getByTestId("target-delimiter-input").fill(","); - - await page.getByTestId("edge-label-column-select").press("Enter"); - await page - .getByRole("option", { name: "Connector Label" }) - .press("Enter"); - - // click test id "submit-button" - await page.getByTestId("submit-button").click(); - - // expect "You are about to add 9 nodes and 10 edges to your graph." to be visible - await expect( - page.getByText( - "You are about to add 9 nodes and 10 edges to your graph." - ) - ).toBeVisible(); - - await page.getByRole("button", { name: "Continue" }).click(); - } catch (error) { - console.error(error); - throw error; - } - }); - - test.afterAll(async () => { - /* This should be run in the last test */ - await deleteCustomerByEmail(email); - console.log("deleted stripe customer: ", email); - }); -}); diff --git a/app/e2e/authenticated.spec.ts b/app/e2e/authenticated.spec.ts new file mode 100644 index 00000000..b8a14c6d --- /dev/null +++ b/app/e2e/authenticated.spec.ts @@ -0,0 +1,194 @@ +import { expect, test } from "@playwright/test"; +import { BASE_URL, goToPath, goToTab } from "./utils"; + +///////////////////////////////////////////////////////////////////////// +// Tests things the user can do when user is logged in but not a pro user +// *and has never been a pro user* +///////////////////////////////////////////////////////////////////////// + +const email = process.env.TESTING_EMAIL; +const password = process.env.TESTING_PASS; +if (!email || !password) + throw new Error("Missing TESTING_EMAIL or TESTING_PASS"); + +// Log In Before Each Test +test.beforeEach(async ({ page }) => { + await goToPath(page); + // Runs before each test and signs in each page. + await page.getByRole("link", { name: "Log In" }).click(); + // fill in testid "sign-in-email" + await page.getByTestId("sign-in-email").fill(email); + // fill in testid "sign-in-password" + await page.getByTestId("sign-in-password").fill(password); + // submit by clicking testid sign-in-email-pass + await page.getByTestId("sign-in-email-pass").click(); + + // wait for the word Account to be visible + await expect(page.getByText("Account")).toBeVisible({ timeout: 10 * 1000 }); +}); + +test("Jump to Pricing from Charts", async ({ page }) => { + await goToTab(page, "Charts"); + + // click test id "to-pricing" + await page.getByTestId("to-pricing").click(); + + // Expect test id pricing-page-title to be visible + await expect( + page.locator('[data-testid="pricing-page-title"]') + ).toBeVisible(); +}); + +// Originally skipped this, failing in CI on Firefox +test("Share Links", async ({ page }) => { + try { + await page.goto(`${BASE_URL}/n#FDC8-YG8F8w0Q`); + const encodedIncludingTheme = `BYUwNmD2AEDukCcwBMBQqC8WuoN6umgCIBjATwBdIBnEgQwAcQBlCssEIgLmIB0A7APQAqYdACakAK7R6-aP0ixZU6lQC2ASwBeIaBVDqQ1aKs38A5tDrQAbnQSa6-CtEgAzaAGFmzAITQAqJiAOogAOS2enTIyCDIspDqRi4mVPrAmiYkkHH6MKBgDNBk0oH8wab8cQhqzgnASvkqakk6eqVSCBkgRgB00MKCAgIiYgAykDHQIAAeFCAI-HRg0O6QqYPD-AACmuoMiK5dYAAUvESC66mCANIOYHR9JNTUFwCUANwjQqLQAKrUPS0V4AWjU7D09kcdAARhw0jBqMAHHoclBatZqm4DIs7CspMYtgIACTXCgAMToWjAZB49yQdG+-BJsLoJAA1hYENJqjwAMTuIXC5msxA1EKaZAGHgABj6AHYAKwMWai2HixZeSAYgWy-UG0WKOIAIXZXJ5Uj50EFwqFori7joUjAFGYKKYPAQIBIFGcFg4zNGf1YkOsEAUuSJ9USLjo5kWJiGAmNelwAkI5PB7R4AEZZarmZmNhRQU6aXToGSS1SK0XoGzOdzechQejEDwSamzU3LdV6xqEDU2zqO1XBzVtRj6+2EF6LLDTrKADTQFdrr4Z6CPWHgHjIOh+0478Cb+T6Oal2AIRg8a+MesLeag9R0Wag2BSmXQA9Hz-S4Az0IBgYmQcwLB4AA2QstyfUt7DATQLH4HgSBAFxFkfS9QRRRDkNQ9CFgQAdNQQD8v2ATsJ0WSUAPrZFGBATtHWdV13UY+tEP4EAcJAJDgAoPM+gAJiVZkAF8fjGaBQw4WM-QTTFkxQkDvRcaB03PBjPWgb1fX9QMtxA2JwJ4FU1S3aiyP-b8xSHGiKKw58EKQlD8gYJzS1w1yCIw4jYOw18EAscxQUrUFoIs884I-G8GB4RRuIkqSQzYOT4gsIllIytMtxsyjx1I2iDHrLNqBzaB8xg88oEgBhQTAvSKE0DY5TiCxOMgOrwVgEAQHi6BhNldrPNBRsLRbUE6vZTQ2DzUbxubK1W1nKjzSW-sAufRa+1bYywMsHgAGZqsILieNWwr7IQKdEEfBxMtLBweVgEddSuydR38rTpAQNDQWepQ3rHOzPunWCHpAJ6EBe8EPSY-QYUsQzz0B17aBWBHcxnLoonBNKEd3bRNEwrcTzAfdDzoY84VPGcvqo0jbu+wgYpc-DZEI0nouw+8Br50qSzLalNFpTtyVrUWyFG7yObQvzRp5P1mta6wpCoJXDxAe7gqhhqsj9fh-vcHl1FBVM81O6BqF+-6wLqI2eJNpJzajOVqsk-hgzEWS9FTJNti4EhMhQBFqAAbXJvwMAuC4AF0NLyijKaPbSQGKwD61AfjBJ-KnTjTgAJPiLAEs9PaDkPkDD8O4IAWQe8xxATzTWcCxv+DClPqfrjvxHLgRK9F6vjAj3vgvMAANFuttLIKQs72Zu9OceF8ngevZQ2rilblQEDxiEOB4ImSe+z2cqDpJDmWxPzxIXGeMPwmQGJ7nCBtrp-vQ5BDnMXPpAoOVOIoIqCgnOjrR6oJv6-xcDwABQCeKgPAQICuQIOC+niLfQgO1JqXWCouXMq58xKlXMJXMCogLbgTMDOcukFynEIZVWUJDBrkMoR-P6PE0Y0PnAQohzDSFsPrFoVsHD-rcLwfQxhxDBEUIgXrCRDM6F8KYSwshcitwiJAZDaGsNJEqJkawjR-AK6qUIlwNBPoFgJF3jg5aPDlGnCGgAFlIaJUhziACclCrIOPwQwhUrjBr6jcd45KKF2TNSiFgtwURGRkFBPtUyTCraQDiY8BJ+ilyrnXLKShaTFgZKmiBEgs1KzylEuEvoqZC4FO9MgZcAg+g5VqXE+IjT+AW0sRgmxW5lqFLoAk6apS5prj6NjPp1QBmZKUf43JOTKH9PiYk0CyTzLhJTFGPo8JzQxLsdUPx9D5kbgHOtXaxSZqjImeeXxWTjl5Ppu9fxok1FKleUqDeqY+iwEyAsPZZzcGzPoS80hbzQUfNOb2SawyynzUsqRQ5i4QWDTBSiiFW47k5IWeEr53I+ryFsQC+xWTGFkMgquAAHMY7BRKDkwquSRa6iLslrmxRioFi57mfK2WQcAUBlCEqhcSjlTjUVDQVEQo6PjaWtnpZWa52CEWYtZSc9lTyjlYtVSYzZcRtlgEJP8oVBysmeIlZVXMQThJeOlUa2VJTYWVUZcOZVuTKFZORR6t53LdWIH9LlG5MrmXItzLmYSq5IIUptRNexcq4U3KVSKrljyxxzM1Q8lBOqQB9AYF0BgclBXRuNSK3MkEgkWvJWiqNG07WXPlU6xYzKk1qpTRqlV6btWdK2fUw1haVoitEuuI6LCh1VvObGx18KmUurZXfRNabvVZu5IMnt1bmXkPXOuohCp200ttRckZdbJ3OrnW2t1J7XVVKagZf1794Zeisdey9LYr3IxvdbO9uln0PtfVUvlmgGBAhiWnHgf6APawzfwPoFAkYBjfcBxGTgf0QezYROgFgNhAY-UwOM6H+BVNALMNDGHd7wYI0RvDyHQAMD9LhzDjEeBUZoxsKpkB9K0ZIx+1jTGKMdr6HUboHH6PWz9GfJpbIYbgDozpcT3owBVLAtSDYvStIfoU+oJTVSohwY-Vpy9jR1AailFJhGCB9OGeQJenOoJTNJHM8Zr0VmbMGcgFKKphxaTsa3PB9zZBcNVJo-Z-QaGn32JfbBwL5ywso09s8dW1nv3hcEzpe+pYovgd4xqCgGhrOTTS4FzL2XIsJei00nIRs6B4zI55lTQmyv0Eq3McjVTSl-XzV5j9oGgRZxLgJZe+UN45T6AeZEmDd7nXxpCSmI2LMQcG8gSAWXRtk2oU-fcC3rHhMGzbRCymzorYJjwbbrnZvIEynx22iCYNtZ+p-LhMMgbweg4h2Dm3TtZpE5Ap716YkfYUfd9GH6vtIY7Vti7ICruIL6kBsH3DHsQ5AX1V7Z3fuliB7BhHb6UcA3+3DITaOOAY6R1msRF1NCtcu896778Yc4-gy1kgBP8fpfPm9qDOi2xk4ZxT77u8sew4-fTxnEOifndu+DynF0eSvGh2L-neP4ckCl9QEXWOmdtiVz99ncudJq8VzQZXJ2zsk-F9esa5p777zgzT2GcOJdm85BbqIKv2dq-Go7zHWvaeA-h27h+IvjdaZl5w7HNudOI8N+99ngfeee9D0J3TEfRfB4WFb2XXu8fh5B6z1XUOY+610Q9wHmeWdG7B9QAAjlIVEQfxHp50hXqv3pnf5-BJX6vefIHa4Rg31E-uweC9T8HrvqFOcle4tn9nA-Nct+H7IUfzOBCg7F2ppTNe7tx50ivzaWfkfs637toLM+68I3333sXwcQC2B5ASrz1vC+1dAFf5jiescX6fzf6Ksf7-Jcf9fs-wfEpB9a8N8EZADm9IFADp9O9j8EoNgF9O1dVfEjsD8kCDtrYdRjsO0vlfFhtQAUCEVVs85pscUtkcD1slt40mVCD5tFsZssDSCEV5spB4Q31UDJsfxpAWCSDECEVIDbEEV8p3YooYsYpqB1AYkyoKoFQ+h1lkMYowArBd5JDdA8xZDeMYpZhVglChZyoVDgkPZM1w5YAZ5zxBC84-wF1w5gATDCBs5S5c5fxqZM4INUxw5qA-obCGxA19g0MT9853CSBR1Jp3BZpUI6l61rJk41wrY2YVgfIGx1skhRp55QpKw1DPYiBUBxJMBsAMAgA`; + + // wait for 2 seconds + await page.waitForTimeout(2000); + + await page.locator(".view-line").first().click(); + await page + .getByRole("textbox", { + name: "Editor content;Press Alt+F1 for Accessibility Options.", + }) + .type("hello world", { delay: 100 }); + + await page.getByRole("button", { name: "Export" }).click(); + + // expect the value of input with testid 'Copy Fullscreen' + expect( + await page + .getByTestId("Copy Fullscreen") + .getAttribute("value") + .then((value) => value?.trim()) + ).toBe(`${BASE_URL}/f#${encodedIncludingTheme}`); + + // 'Copy Editable' + expect( + await page + .getByTestId("Copy Editable") + .getAttribute("value") + .then((value) => value?.trim()) + ).toBe(`${BASE_URL}/n#${encodedIncludingTheme}`); + + // 'Copy Read-only + expect( + await page + .getByTestId("Copy Read-only") + .getAttribute("value") + .then((value) => value?.trim()) + ).toBe(`${BASE_URL}/c#${encodedIncludingTheme}`); + } catch (error) { + console.log(error); + // grab screenshot + await page.screenshot({ path: `test-results/share-links-error.png` }); + throw error; + } +}); + +test("Create New Local Chart", async ({ page }) => { + await goToTab(page, "New"); + + await page.getByRole("link", { name: "New" }).click(); + await page.getByPlaceholder("Untitled").click(); + await page.getByPlaceholder("Untitled").press("Meta+a"); + await page.getByPlaceholder("Untitled").fill("My New Chart"); + await page + .getByRole("radio", { + name: "Temporary Stored on this computer Deleted when browser data is cleared", + }) + .click(); + await page.getByRole("button", { name: "Create" }).click(); + await expect(page).toHaveURL(`${BASE_URL}/my-new-chart`); +}); + +test("Go to Charts Page", async ({ page }) => { + await goToTab(page, "Charts"); + + // click the link with text "/" + await page.getByRole("link", { name: "/" }).click(); + + await expect(page).toHaveURL(`${BASE_URL}/`); +}); + +test("Clone a Chart", async ({ page }) => { + await goToTab(page, "Charts"); + + // click element with aria label "Clone" + await page.getByRole("button", { name: "Copy flowchart: /" }).click(); + + await expect(page).toHaveURL(`${BASE_URL}/Untitled-1`); + + await expect(page.locator("text=-1")).toBeVisible(); +}); + +test("Delete a chart", async ({ page }) => { + await goToTab(page, "New"); + + await page.getByPlaceholder("Untitled").click(); + await page.getByPlaceholder("Untitled").press("Meta+a"); + await page.getByPlaceholder("Untitled").fill("to delete"); + + // click the button with the role "radio" that contains the text "Temporary" + await page + .getByRole("radio", { + name: "Temporary Stored on this computer Deleted when browser data is cleared", + }) + .click(); + + await page.getByRole("button", { name: "Create" }).click(); + + await goToTab(page, "Charts"); + + await page + .getByRole("button", { name: "Delete flowchart: /to-delete" }) + .click(); + await page.getByRole("button", { name: "Delete" }).click(); + + // expect "delete-me" NOT to be in the document + await expect(page.locator("text=to-delete")).not.toBeVisible(); +}); + +// Skip completely "Navigation interrupted by another one" +test.skip("Create new chart from a template", async ({ page, browserName }) => { + // Firefox has a weird bug, most likely due to the "#" in the URL + test.skip(browserName === "firefox", "Firefox has a weird bug"); + + // Go to url + await page.goto( + `${BASE_URL}/n#C4ewBARgpmCWB2ZgAsYBMQGMCuBbK8wAUALxllEDeRYYARAA4CGATgM5Qt0Bc9A5iyYNkAWg4AbKJlBciAX1LkSQA` + ); + + // expect "to be in the document" to be in the document + await expect( + page.locator("text=to be in the document").first() + ).toBeVisible(); + + // expect the url to contain "temp" in it + expect(page.url()).toContain("temp"); +}); + +test("Rename chart", async ({ page }) => { + // Click [aria-label="Rename"] + await page.locator('[aria-label="Rename"]').click(); + // Fill input[name="name"] + await page.locator('input[name="name"]').fill("my new chart"); + // Click button:has-text("Rename") + await page.locator('button:has-text("Rename")').click(); + await expect(page).toHaveURL(`${BASE_URL}/my-new-chart`); + // Click text=my-new-chart + await expect(page.locator("text=my-new-chart")).toBeVisible(); + // Click [aria-label="Rename"] + await page.locator('[aria-label="Rename"]').click(); + // Press a with modifiers + await page.locator('input[name="name"]').press("Meta+a"); + // Fill input[name="name"] + await page.locator('input[name="name"]').fill("cool chart"); + // Click button:has-text("Rename") + await page.locator('button:has-text("Rename")').click(); + await expect(page).toHaveURL(`${BASE_URL}/cool-chart`); + // Click text=cool-chart + await expect(page.locator("text=cool-chart")).toBeVisible(); +}); diff --git a/app/e2e/graph-floating-menu.spec.ts b/app/e2e/graph-floating-menu.spec.ts index ffbd2cdb..1a8c73ff 100644 --- a/app/e2e/graph-floating-menu.spec.ts +++ b/app/e2e/graph-floating-menu.spec.ts @@ -17,7 +17,6 @@ test.describe("Graph Floating Menu", () => { try { await page.getByTestId("Zoom Out").click(); await page.getByTestId("Zoom In").click(); - await page.getByTestId("Fit Graph").click(); } catch (error) { console.log(error); // grab screenshot diff --git a/app/e2e/heads-up.spec.ts b/app/e2e/heads-up.spec.ts index a5d91e72..b52e1882 100644 --- a/app/e2e/heads-up.spec.ts +++ b/app/e2e/heads-up.spec.ts @@ -15,12 +15,17 @@ test.describe("Click Heads Up", () => { test("Click Heads Up", async ({ page }) => { try { + // click the aria-label "Temporary Flowchart Warning" await page - .getByText( - "Heads up! Before you clear your cache, remember that this document isn't saved i" - ) + .getByRole("button", { name: "Temporary Flowchart Warning" }) .click(); - await page.getByRole("link", { name: "Learn More" }).click(); + + // expect "This document is only saved on this computer" to be visible + await expect(page.locator("text=document is only saved")).toBeVisible(); + + // click testid might-lose-warning-learn-more + await page.getByTestId("might-lose-warning-learn-more").click(); + // expect to be at /pricing await expect(page).toHaveURL(`${BASE_URL}/pricing`); } catch (error) { diff --git a/app/e2e/monthly.spec.ts b/app/e2e/monthly.spec.ts deleted file mode 100644 index 040d602b..00000000 --- a/app/e2e/monthly.spec.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { expect, test } from "@playwright/test"; -import jsdom from "jsdom"; - -import { - BASE_URL, - deleteCustomerByEmail, - getTempEmail, - getTempEmailMessage, - goToPath, -} from "./utils"; - -test.describe("Monthly Sign Up", () => { - test.beforeEach(async ({ page }) => { - await goToPath(page); - }); - - test("Monthly Sign-up", async ({ page }) => { - // set timeout - test.setTimeout(240000); - - await page.getByRole("link", { name: "Pricing" }).click(); - await expect(page).toHaveURL(`${BASE_URL}/pricing`); - - await page.getByTestId("monthly-plan-button").click(); - await page.getByRole("button", { name: "Continue" }).click(); - - const email = await getTempEmail(); - await page.getByTestId("email-input").fill(email); - await page.getByRole("button", { name: "Continue" }).click(); - - const iframe = page.frameLocator( - 'internal:attr=[title="Secure payment input frame"i]' - ); - - await iframe - .getByPlaceholder("1234 1234 1234 1234") - .fill("4242 4242 4242 4242"); - await iframe.getByPlaceholder("MM / YY").fill("05 / 50"); - - await iframe.getByPlaceholder("CVC").fill("222"); - await iframe.getByRole("combobox", { name: "Country" }).selectOption("US"); - await iframe.getByPlaceholder("12345").fill("12345"); - - await page.getByRole("button", { name: "Sign Up" }).click(); - - // expect the url to start with "/l" - await expect(page).toHaveURL(new RegExp(`${BASE_URL}/l`)); - - // expect sign up success to be on the screen - await page - .getByRole("heading", { name: "Welcome to Flowchart Fun Pro!" }) - .click(); - - /* Part 2: Get Auth Email */ - await page.getByLabel("Email").fill(email); - await page.getByRole("button", { name: "Submit" }).click(); - await expect( - page.getByText( - "Check your email for a link to log in. You can close this window." - ) - ).toBeVisible({ timeout: 60000 }); - - // wait for email - let emails = [], - i = 0; - while (emails.length === 0 && i < 20) { - // Wait 5 seconds - await new Promise((resolve) => setTimeout(resolve, 10000)); - // Check for email - const response = await getTempEmailMessage(email); - if (!("error" in response)) { - emails = response; - } - i++; - } - - expect(emails.length).toBeGreaterThanOrEqual(1); - const signInEmailIndex = emails.findIndex((email: { mail_html: string }) => - /supabase.co\/auth\/v1\/verify/.test(email.mail_html) - ); - - expect(signInEmailIndex).toBeGreaterThanOrEqual(0); - - // apply this to a new document to get link and then load link and confirm the Account link present - const html = emails[signInEmailIndex].mail_html; - const { window } = new jsdom.JSDOM(html); - const { document } = window; - const link = document.querySelector("a"); - expect(link).toBeTruthy(); - expect(link?.href).toBeTruthy(); - await page.goto(link?.href as string); - - // expect link with "Account" to be present - await expect(page.getByText("Account")).toBeVisible({ timeout: 10 * 1000 }); - - // delete customer - await deleteCustomerByEmail(email); - console.log("deleted stripe customer: ", email); - }); -}); diff --git a/app/e2e/open-syntax-reference.spec.ts b/app/e2e/open-syntax-reference.spec.ts index 61583f49..b7e00fc1 100644 --- a/app/e2e/open-syntax-reference.spec.ts +++ b/app/e2e/open-syntax-reference.spec.ts @@ -15,9 +15,6 @@ test.describe("Open syntax reference", () => { test("Open syntax reference", async ({ page }) => { try { - await page - .getByRole("combobox", { name: "Syntax" }) - .selectOption("graph-selector"); await page.getByRole("button", { name: "Learn Syntax" }).click(); // expect 'Node Label' to be in the document await expect( diff --git a/app/e2e/pro.spec.ts b/app/e2e/pro.spec.ts new file mode 100644 index 00000000..97972086 --- /dev/null +++ b/app/e2e/pro.spec.ts @@ -0,0 +1,277 @@ +import { expect, Page, test } from "@playwright/test"; +import jsdom from "jsdom"; +import path from "path"; + +import { openExportDialog } from "./openExportDialog"; +import { + BASE_URL, + deleteCustomerByEmail, + getTempEmail, + getTempEmailMessage, +} from "./utils"; + +test.describe.configure({ + mode: "serial", +}); + +let email = ""; + +let page: Page; + +// temporarily skip these tests, out of rapidapi quota +test.skip(() => true, "Exceeded RapidAPI quota"); + +test.beforeAll(async ({ browser }) => { + page = await browser.newPage(); + await page.goto(BASE_URL); +}); + +test.afterAll(async () => { + await page.close(); +}); + +test("Sign Up", async () => { + // set timeout + test.setTimeout(240000); + + await page.getByRole("link", { name: "Pricing" }).click(); + await expect(page).toHaveURL(`${BASE_URL}/pricing`); + + await page.getByTestId("yearly-plan-button").click(); + await page.getByRole("button", { name: "Continue" }).click(); + + email = await getTempEmail(); + await page.getByTestId("email-input").fill(email); + await page.getByRole("button", { name: "Continue" }).click(); + + const iframe = page.frameLocator( + 'internal:attr=[title="Secure payment input frame"i]' + ); + + await iframe + .getByPlaceholder("1234 1234 1234 1234") + .fill("4242 4242 4242 4242"); + await iframe.getByPlaceholder("MM / YY").fill("05 / 50"); + + await iframe.getByPlaceholder("CVC").fill("222"); + await iframe.getByRole("combobox", { name: "Country" }).selectOption("US"); + await iframe.getByPlaceholder("12345").fill("12345"); + + await page.getByRole("button", { name: "Sign Up" }).click(); + + // expect the url to start with "/l" + await expect(page).toHaveURL(new RegExp(`${BASE_URL}/l`)); + + // expect sign up success to be on the screen + await page + .getByRole("heading", { name: "Welcome to Flowchart Fun Pro!" }) + .click(); + + /* Part 2: Get Auth Email */ + // fill in testid sign-in-magic-email with email + await page.getByTestId("sign-in-magic-email").fill(email); + + // get button with test id "request-magic-link" + await page.getByTestId("request-magic-link").click(); + await expect( + page.getByText(/Check your email for a link to log in/i) + ).toBeVisible({ timeout: 60000 }); + + // wait for email + let emails = [], + i = 0; + while (emails.length === 0 && i < 20) { + // Wait 5 seconds + await new Promise((resolve) => setTimeout(resolve, 10000)); + // Check for email + const response = await getTempEmailMessage(email); + if (!("error" in response)) { + emails = response; + } + i++; + } + + expect(emails.length).toBeGreaterThanOrEqual(1); + const signInEmailIndex = emails.findIndex((email: { mail_html: string }) => + /supabase.co\/auth\/v1\/verify/.test(email.mail_html) + ); + + expect(signInEmailIndex).toBeGreaterThanOrEqual(0); + + // apply this to a new document to get link and then load link and confirm the Account link present + const html = emails[signInEmailIndex].mail_html; + const { window } = new jsdom.JSDOM(html); + const { document } = window; + const link = document.querySelector("a"); + expect(link).toBeTruthy(); + expect(link?.href).toBeTruthy(); + await page.goto(link?.href as string); + + // reload page + await page.reload(); + + // expect link with "Account" to be present + await expect(page.getByText("Account")).toBeVisible({ timeout: 60 * 1000 }); +}); + +test("Publish Chart", async () => { + // page + await page.getByRole("link", { name: "New" }).click(); + + // Make a new hosted chart + await page.getByPlaceholder("Untitled").fill("my new chart"); + await page + .getByRole("radio", { + name: "Persistent Stored in the cloud Accessible from any device", + }) + .click(); + + await page.getByRole("button", { name: "Create" }).click(); + + // expect url to be regex BASE_URL + /u/\d+ + await expect(page).toHaveURL(new RegExp(`${BASE_URL}/u/\\d+`)); + + await page.getByRole("button", { name: "Export" }).click(); + await page.getByLabel("Make publicly accessible").check(); + + // read the value from the textbox with the name 'Copy Public Link' + const publicLink = page.getByRole("textbox", { + name: "Copy Public Link", + }); + + const publicLinkValue = await publicLink.getAttribute("value"); + + if (!publicLinkValue) throw new Error("Public link value is empty"); + + // navigate to public url + await page.goto(publicLinkValue); + + // expect url to be regex BASE_URL + /p/\w+-\w+-\w+ + await expect(page).toHaveURL(new RegExp(`${BASE_URL}/p/\\w+-\\w+-\\w+`)); + + // expect Clone button to be present + await expect(page.getByRole("button", { name: "Clone" })).toBeVisible(); +}); + +test("Download SVG", async () => { + // Create a blank local chart + await page.goto(`${BASE_URL}/download-svg`); + await openExportDialog(page); + // Click [aria-label="Download SVG"] + const [download] = await Promise.all([ + page.waitForEvent("download"), + page.locator('[aria-label="Download SVG"]').click(), + ]); + + expect(download.suggestedFilename()).toBe("download-svg.svg"); +}); + +test("Convert chart to hosted from Might Lose Trigger", async () => { + // Create a blank local chart + await page.goto(`${BASE_URL}/my-new-chart`); + + // Hover [data-testid="might-lose-sponsor-trigger"] then wait for the button to appear + await page.getByTestId("might-lose-sponsor-trigger").click(); + + // Make sure the input with the label Convert to hosted chart? is checked + await page.getByTestId("convert-to-hosted").click(); + + // Add a character to make name different + await page.getByRole("textbox").click(); + await page.getByRole("textbox").fill("my-new-chart-"); + + // Rename + await page.getByRole("button", { name: "Rename" }).click(); + + // expect "/u/" to be in the url + await expect(page).toHaveURL(new RegExp(`${BASE_URL}/u/\\d+`)); +}); + +test("Create chart from prompt by instruction", async () => { + await page.getByRole("link", { name: "New" }).click(); + await page.getByRole("radio", { name: "Prompt" }).click(); + await page.getByTestId("instruct").click(); + await page.getByTestId("prompt-entry-textarea").click(); + await page + .getByTestId("prompt-entry-textarea") + .fill("the stages of the water cycle"); + await page.getByRole("button", { name: "Create" }).click(); + // expect url to be regex BASE_URL + /u/\d+ + await expect(page).toHaveURL(new RegExp(`${BASE_URL}/u/\\d+`), { + timeout: 12000, + }); +}); + +test("Create chart from prompt by extraction", async () => { + await page.getByRole("link", { name: "New" }).click(); + await page.getByRole("radio", { name: "Prompt" }).click(); + await page.getByTestId("extract").click(); + await page.getByTestId("prompt-entry-textarea").click(); + await page + .getByTestId("prompt-entry-textarea") + .fill("a is greater than b but less than a"); + await page.getByRole("button", { name: "Create" }).click(); + // expect url to be regex BASE_URL + /u/\d+ + await expect(page).toHaveURL(new RegExp(`${BASE_URL}/u/\\d+`), { + timeout: 12000, + }); +}); + +test("Create chart from imported data", async () => { + try { + await page.getByRole("button", { name: "Import Data" }).click(); + + const filePath = path.join( + __dirname, + "../../api/data/fixtures/example-visio-process.csv" + ); + + // make the file input visible + // execute this code on the page: document.querySelector("[data-testid=import-data-file-uploader]").style.display = "block" + await page.evaluate(() => { + const fileInput = document.querySelector( + "[data-testid=import-data-file-uploader]" + ) as HTMLElement; + fileInput.style.display = "block"; + }); + + // click the text "Drag and drop a CSV file here, or click to select a file" + // await page.getByText("Drag and drop a CSV file here").click(); + // await page.getByTestId("import-data-file-uploader").click(); + await page.getByTestId("import-data-file-uploader").setInputFiles(filePath); + + await page.getByTestId("node-label-select").press("Enter"); + await page + .getByRole("option", { name: "Process Step Description" }) + .press("Enter"); + + await page.getByTestId("edges-in-source-node-row").click(); + + await page.getByTestId("target-column-select").press("Enter"); + await page.getByRole("option", { name: "Next Step ID" }).press("Enter"); + + await page.getByTestId("target-delimiter-input").fill(","); + + await page.getByTestId("edge-label-column-select").press("Enter"); + await page.getByRole("option", { name: "Connector Label" }).press("Enter"); + + // click test id "submit-button" + await page.getByTestId("submit-button").click(); + + // expect "You are about to add 9 nodes and 10 edges to your graph." to be visible + await expect( + page.getByText("You are about to add 9 nodes and 10 edges to your graph.") + ).toBeVisible(); + + await page.getByRole("button", { name: "Continue" }).click(); + } catch (error) { + console.error(error); + throw error; + } +}); + +test.afterAll(async () => { + /* This should be run in the last test */ + await deleteCustomerByEmail(email); + console.log("deleted stripe customer: ", email); +}); diff --git a/app/e2e/share-links.spec.ts b/app/e2e/share-links.spec.ts deleted file mode 100644 index 02500801..00000000 --- a/app/e2e/share-links.spec.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { expect, test } from "@playwright/test"; - -import { BASE_URL, goToPath } from "./utils"; - -/* -Run single test file -pnpm playwright test e2e/share-links.spec.ts --headed --project=chromium -*/ - -// Make sure share links are never broken -test.describe("share-links", () => { - test.beforeEach(async ({ page }) => { - await goToPath(page); - }); - - test.skip("share links", async ({ page }) => { - try { - await page.goto(`${BASE_URL}/n#FDC8-YG8F8w0Q`); - const encoded = `BYUwNmD2AEDukCcwBMBQqC8WuoN6umgCIAHAQwQGcQEiAuYgNwEYjUBfTbDIA`; - - // wait for 2 seconds - await page.waitForTimeout(2000); - - await page.locator(".view-line").first().click(); - await page - .getByRole("textbox", { - name: "Editor content;Press Alt+F1 for Accessibility Options.", - }) - .type("hello world", { delay: 100 }); - - await page.getByRole("button", { name: "Export" }).click(); - - // expect the value of input with testid 'Copy Fullscreen' - expect( - await page - .getByTestId("Copy Fullscreen") - .getAttribute("value") - .then((value) => value?.trim()) - ).toBe(`${BASE_URL}/f#${encoded}`); - - // 'Copy Editable' - expect( - await page - .getByTestId("Copy Editable") - .getAttribute("value") - .then((value) => value?.trim()) - ).toBe(`${BASE_URL}/n#${encoded}`); - - // 'Copy Read-only - expect( - await page - .getByTestId("Copy Read-only") - .getAttribute("value") - .then((value) => value?.trim()) - ).toBe(`${BASE_URL}/c#${encoded}`); - } catch (error) { - console.log(error); - // grab screenshot - await page.screenshot({ path: `test-results/share-links-error.png` }); - throw error; - } - }); -}); diff --git a/app/e2e/unauth.spec.ts b/app/e2e/unauth.spec.ts index b9345907..9a2be6ac 100644 --- a/app/e2e/unauth.spec.ts +++ b/app/e2e/unauth.spec.ts @@ -1,7 +1,7 @@ import { expect, test } from "@playwright/test"; import { openExportDialog } from "./openExportDialog"; -import { BASE_URL, changeEditorText, goToPath, goToTab } from "./utils"; +import { BASE_URL, goToPath, goToTab } from "./utils"; /* Run single test file @@ -11,401 +11,215 @@ pnpm playwright test e2e/unauth.spec.ts --headed --project=chromium // Run in parallel test.describe.configure({ mode: "parallel" }); -test.describe("unauth", () => { - test.beforeEach(async ({ page }) => { - await goToPath(page); - }); - - test.only("View Pricing Page", async ({ page }) => { - await goToTab(page, "Charts"); - - // click test id "to-pricing" - await page.getByTestId("to-pricing").click(); - - // Expect test id pricing-page-title to be visible - await expect( - page.locator('[data-testid="pricing-page-title"]') - ).toBeVisible(); - }); - - test("Create New Local Chart", async ({ page }) => { - await goToTab(page, "New"); - - await page.getByRole("link", { name: "New" }).click(); - await page.getByPlaceholder("Untitled").click(); - await page.getByPlaceholder("Untitled").press("Meta+a"); - await page.getByPlaceholder("Untitled").fill("My New Chart"); - await page - .getByRole("radio", { - name: "Temporary Stored on this computer Deleted when browser data is cleared", - }) - .click(); - await page.getByRole("button", { name: "Create" }).click(); - await expect(page).toHaveURL(`${BASE_URL}/my-new-chart`); - }); - - test("Open a Chart", async ({ page }) => { - await goToTab(page, "Charts"); - await page.click('a:has-text("/")'); - await expect(page).toHaveURL(`${BASE_URL}/`); - }); - - test("Clone a Chart", async ({ page }) => { - await goToTab(page, "Charts"); - - // click element with aria label "Clone" - await page.getByRole("button", { name: "Copy flowchart: /" }).click(); - - await expect(page).toHaveURL(`${BASE_URL}/-1`); - - await expect(page.locator("text=-1")).toBeVisible(); - }); - - test("Delete a chart", async ({ page }) => { - await goToTab(page, "New"); - - await page.getByPlaceholder("Untitled").click(); - await page.getByPlaceholder("Untitled").press("Meta+a"); - await page.getByPlaceholder("Untitled").fill("to delete"); - await page.getByRole("button", { name: "Create" }).click(); +test.beforeEach(async ({ page }) => { + await goToPath(page); +}); - await goToTab(page, "Charts"); +test("Download PNG", async ({ page }) => { + await openExportDialog(page); + // Click [aria-label="Download PNG"] + const [download] = await Promise.all([ + page.waitForEvent("download"), + page.locator('[aria-label="Download PNG"]').click(), + ]); - await page - .getByRole("button", { name: "Delete flowchart: /to-delete" }) - .click(); - await page.getByRole("button", { name: "Delete" }).click(); + expect(download.suggestedFilename()).toBe("flowchart-fun.png"); +}); - // expect "delete-me" NOT to be in the document - await expect(page.locator("text=to-delete")).not.toBeVisible(); - }); +test("Download JPG", async ({ page }) => { + await openExportDialog(page); + // Click [aria-label="Download JPG"] + const [download] = await Promise.all([ + page.waitForEvent("download"), + page.locator('[aria-label="Download JPG"]').click(), + ]); - test("Create a New Local Chart", async ({ page }) => { - await changeEditorText(page, "1"); + expect(download.suggestedFilename()).toBe("flowchart-fun.jpg"); +}); - expect(new URL(page.url()).pathname).toBe("/"); +test("Copy Fullscreen Link", async ({ page }) => { + await openExportDialog(page); - await page.getByRole("link", { name: "New" }).click(); - await page.getByPlaceholder("Untitled").click(); - await page.getByPlaceholder("Untitled").fill("a-b-c-d-e"); + // Expect input to have the correct value + await expect( + page.locator('text=FullscreenCopy >> input[type="text"]') + ).toHaveValue( + `${BASE_URL}/f#CoCwlgzgBAhgDnKB3A9gJwNbQEYE8oAuucYAdgOYBQUUAkqQCYCmpBZ5UAxmkzAU9BikoTBuSbVYpfPwAeBAFxRsTAGbomsLigA2KYd179BUHTBU7JNAMI6wnDFH1QAOqQAyvNMIDKuVjCybgQopl7CALYaUAB02DoArkxx6MxoALSk+sloKAmMPJwEQuQ6TADaSAC8AIwADHUAuuUgVQDsTZIAmnlcQrA6EKFIaPCETPJQZFAARC4AFK4AlDOEoYZ8mv2i4lZQ8yrqPFqcus4bxlpmFkuUlFUPD5QA3pIznLghEJzwTD5EZRmSjmpAA9AAqcFQHoJPrCLJILgJCAhCJgABemgIICYEQEUGR7C0ADcYGgwEICE5VFBrD4fABCVykSFQgDqTAA5MStgxmAxtBE8axoCFCOBoKdmGsoDidIhcL03KyCYwmGgUUIBSAUIixZxkaiMZpFQk0OLcckoODQW43BCoe4UDABRN+N4YDooOoRdbbaQAAJgCJwdBUs06eYuGagn0ECCggDSZLMMU4EAg0aWAG47WDIVAAKoQTTfDPpFG4MpQUnk8xlUWhCAgMmaU56DVSAUobHqmuepLQG1uAAkcYAYjA0TpcEpk2gzLnSCPsDAHORcvkGEoAMSqfcHpcr1LqtlgBjYpR1GJtACscFkR+wJ7Q1jOaF3DS-dSPWWYACE1wwDc8kYXcD0PUdmFUGAEh0AgfBbOAmCUQpigoMol3tAt-irLYdC9P98S1bQAjIdUh39IioGeNwaDjCtjSUeoHyXej9AIdIYOnWcoDHDjJx4tjlCAkCt3Sdt0CUEciMA9dN0YYTnzQNIJPfaTlLSN8O2EySPygNByGweY6gAGigMyLJzOjTHMJgdCUBg+BgeZrns6zhHGeR0hGeAlF8uBhLkTiIkCHzz0vKAnOKeYkAikAPJoOAXQYdglAANlYmzgvSUk7HIUglE4Fh3SCt10hbfLCq4Er1SUl9wovEANJfM8muE5tfmk6DYPgxDfmEuxSCYCqmDAcgQEUKAahiAAmW8lwAXzzB0oFw6tTjI4bO2HQrkp4VgaJszrkNQpgihKTCbOSvk0qge9HxszT1UayLjxU094rK7y8vG6qQkC7Lysqv6itqtBvpCslyDIdJePSTLHs8nKAqULJhqWlacIBTQdnxXa8aOzy4qalqPrQNrsWEhiICY6a6iyzy9BQOB0lStCwH0K9mHIQaUBZiskCYJg4CUWa6h5yH0lXeTQIYdIWbXMAiGYqWZeAhT5b0jTRM1tXdbl9IbtSiglAAZkZmghpG7W+Oe193yC6GmE4slciQNSOzJrTHeOvI0GK9I3d1T2pLtl9tPQJ3DJdoO0HdiskJQwg6wwphhODj3vk9ZOal0s0eQrHGlBUdEwDqmy3IcqLnNcuydES7QvfD8nI4hoGfs9UGatYCvkfK1HkFGQHPIY7iwBnaSJynCfcClkGCrB3v2-77zcmKNgudgBIQnXzZo-ETjUs1UhA9UXIIkyFBmGYy2oCGM1A+P9Cz4vq+b4sxnltIbCoXWzQiKUTcAoTg4AdAMAbBAcoVcGRVGjNGRoRMaAk0itFFyJ0mCUwSsJHE41JqOVrhggAEmNCaBAPLfxAWAiBAgoHBQALLQzIF0RBtFV5Q0MrDXiaD5gMKYaQLoFDgGgInjQjM5Q+GcNIAADVYR3DhMNSDpFkAQmKkjFHSKET-QqzNEBsJoAaNAhdKxlBLkwMufdv54xASgEMcskFIiMSNExydS7lxXjQB+AcRosAYKGMgU08jxnPCNEI6RrYH1jr4-xrAlBBNpswdIYSIluEoSWMoRRRAOPVmJRgod9KGWMjUcy9RbzmVmjUNojdrb5NQkZeYxT6ZlKgBUqpHV-aB0zrUgy9TGmlPKZUxuaJ5ZeM6fHEOttCkNJKXUZprTG7FBjq7cZHtJm9JmXMwZwlhlJOdsshOayikbIGW01JwD9olQUOk86-ABT6JErLcShz5jiwACzlPmuU15ABORu9tulTMqe8lpDQPm-MxoVNcbAeQOJQDyBcMBcBGxSndFiSMaBwvVGYJFzzLJ4sbpihFSLFacGVrxa880IUxCIkQwlohTJuBiHjWl8L6VuCIlc+yNysn3K3FixFCtkqkpVhZGIecbJ8qJQC+peLzJ1EbpK7FyLbqm3ul-PMRE4hmAcNkg2Tz1I9OMrKqySk9V5JJWS1WT0Gq4rlXKxuzz5pzNvM628WjNVIHAPwXVjy8mOpdeUgNLSXV-LNfLC1IrxWeX+f611ga3W6QNVM418qIWao3MLYQ9ycma2lUclpNR0rmQABynOjWGwVStI31XJnmkydqTU2VtRZe1abr7JFwPZPQiJs0VtjeUuobQSlm1Db68NQrLXTRrapZt+LE3N2TQ21NZzSCaviEkH1GtDbPO+UO6aNRgWzR+aOrd4kI28SjTQGNSaZVLodTe4yTr43xvde2mI6ASiaF7WOutT792zXMulYtJ7cnjqrRe6dL1Z2tqbQ++tLbG2kG-pquAZo4DVm-aev1cHC3AoPUW4NCanoVvPVa6NNq4MpvvQu29CHl1IfZW+ngdziM-v9ZZM2zTOMgdzaRqd1ra3QcQwYyjd623MBiBuRFm7QN1sqZZeTJSOg8cNnxy9ygKM0aNWJ2DWn4NzpXTENCl0v3HSTmdC6acqWa2M2nBxGDUJy1s6UdOhmu1gDgCWez5mRAEQ8yWKlBBU4ue811FOFIrOGeQgEcgzh7kOagNF4osXSBUpxLIGAKXQunVlBMTL+g0si2S3FszYWcRwGK6lwzKALpZfiz5mrlWqWanNPVsLLWqWrnjvZbLri3b2SpalKc+gWOeQS0NqIilDM8lM2NnzM3rM6giM+c8vXUJLZWwwazeDOJoA2ygVbbWcvkjIekPbtjNtUtDDOOrpWcvXdwClwLmW1uEEy9Zw2zmMN3eTrmr7rmGOkDTDvM7NyTOvYNLtsHkXAcpAIKiM7n3ochaO64nsCO-vI6urDzaPxC7pfy1mn7RV9B45GgTp7hnSUB2+3NsL7nPMA5oLgshqiXIoOwSuvGMQnLNh5ZXciRc8IEL51trnYhkgMHR-zpmguXGOWl2LwH3Ohh2FG1bOXxd766HPBClXHTQnBdp54g3ccE4JaCxFlzeuJcxEWYfJJRvZs0Ht7HLpFunc2-EDEUZhurdlCScLezpv3c+ctyZwPAOrG29d5xcPadI8ONj2bkOHv-ehOFl75IvuJJgBp378H8WQ8rMTmF6nnAA-x+t+L73yfy+V6d0nvZKes4+frwXmH0fvc56rwH7gKAMzB8fiNUPYXe821yBmLPdvm-j4kpP6A9zk+j5y3P-vU+a-Z9N3P9WhiYVF+Hy30vq+nfSyAnvqPbhufJ53+fguzu3tLKP2niPu-7-T5zzNof3jn-zcz5vmfJ-L-JfZvFfZOBbAAnvIPA-H-MApQfgS-YaGPWfaA7KUAkvC3f-ZXW3HPCAAARwSFbG-zGXNx83wMIJ4Gn2T3IKIJAKfzgPvgINbA-1N3b2IJHwwLbzzwr0QOv2bzYLoIdwYPbxYMPwmxG3YN-zC3EKm2wNr2bxkPV0fyEM4OkIpEmyVy7y30P1ASYGJFyCJzG2L1ILLxxH0IKwALrzMIMKb3oNUJy10PMKqzkO0J-3RgfxzwYPcKoOb3cNsJUJMJy28JXTXQalV0OwE1Unl21zV3E2SH+V5xxCUP+WiMSNEDiJSFrSl3hxlyvTCK12yNuQyISLyHiAfxSIKNKOx2QzfX+T8OzQag5yvHVSBxyggAiAcRpjpjaBiAekC3Kh0A4HuS6MxGYj6MMxylkC9GGI4kYlGJBRaKIkqDkWJnijZ1ilfWYBaBWOZ1IXwRrhik50ByWIgADh2IeSw3lmDEy2Th4VOM4BU3ElUGVhJ3hUgwyCaM-nRS8k4l+kXg03h1sSllCikThiUHGMBxmEoEWnuEeCqCAA` + ); - await page - .getByRole("radio", { - name: "Temporary Stored on this computer Deleted when browser data is cleared", - }) - .click(); + await page.locator('[aria-label="Copy Fullscreen"]').click(); + // await expect(page.locator('[data-testid="Copied Fullscreen"]')).toBeVisible(); +}); - await page.getByRole("button", { name: "Create" }).click(); +test("Copy With-Editor Link", async ({ page }) => { + await openExportDialog(page); - // Make sure we're on the new chart - expect(new URL(page.url()).pathname).toBe("/a-b-c-d-e"); + // Expect input to have the correct value + await expect( + page.locator('text=FullscreenCopy >> input[type="text"]') + ).toHaveValue( + `${BASE_URL}/f#CoCwlgzgBAhgDnKB3A9gJwNbQEYE8oAuucYAdgOYBQUUAkqQCYCmpBZ5UAxmkzAU9BikoTBuSbVYpfPwAeBAFxRsTAGbomsLigA2KYd179BUHTBU7JNAMI6wnDFH1QAOqQAyvNMIDKuVjCybgQopl7CALYaUAB02DoArkxx6MxoALSk+sloKAmMPJwEQuQ6TADaSAC8AIwADHUAuuUgVQDsTZIAmnlcQrA6EKFIaPCETPJQZFAARC4AFK4AlDOEoYZ8mv2i4lZQ8yrqPFqcus4bxlpmFkuUlFUPD5QA3pIznLghEJzwTD5EZRmSjmpAA9AAqcFQHoJPrCLJILgJCAhCJgABemgIICYEQEUGR7C0ADcYGgwEICE5VFBrD4fABCVykSFQgDqTAA5MStgxmAxtBE8axoCFCOBoKdmGsoDidIhcL03KyCYwmGgUUIBSAUIixZxkaiMZpFQk0OLcckoODQW43BCoe4UDABRN+N4YDooOoRdbbaQAAJgCJwdBUs06eYuGagn0ECCggDSZLMMU4EAg0aWAG47WDIVAAKoQTTfDPpFG4MpQUnk8xlUWhCAgMmaU56DVSAUobHqmuepLQG1uAAkcYAYjA0TpcEpk2gzLnSCPsDAHORcvkGEoAMSqfcHpcr1LqtlgBjYpR1GJtACscFkR+wJ7Q1jOaF3DS-dSPWWYACE1wwDc8kYXcD0PUdmFUGAEh0AgfBbOAmCUQpigoMol3tAt-irLYdC9P98S1bQAjIdUh39IioGeNwaDjCtjSUeoHyXej9AIdIYOnWcoDHDjJx4tjlCAkCt3Sdt0CUEciMA9dN0YYTnzQNIJPfaTlLSN8O2EySPygNByGweY6gAGigMyLJzOjTHMJgdCUBg+BgeZrns6zhHGeR0hGeAlF8uBhLkTiIkCHzz0vKAnOKeYkAikAPJoOAXQYdglAANlYmzgvSUk7HIUglE4Fh3SCt10hbfLCq4Er1SUl9wovEANJfM8muE5tfmk6DYPgxDfmEuxSCYCqmDAcgQEUKAahiAAmW8lwAXzzB0oFw6tTjI4bO2HQrkp4VgaJszrkNQpgihKTCbOSvk0qge9HxszT1UayLjxU094rK7y8vG6qQkC7Lysqv6itqtBvpCslyDIdJePSTLHs8nKAqULJhqWlacIBTQdnxXa8aOzy4qalqPrQNrsWEhiICY6a6iyzy9BQOB0lStCwH0K9mHIQaUBZiskCYJg4CUWa6h5yH0lXeTQIYdIWbXMAiGYqWZeAhT5b0jTRM1tXdbl9IbtSiglAAZkZmghpG7W+Oe193yC6GmE4slciQNSOzJrTHeOvI0GK9I3d1T2pLtl9tPQJ3DJdoO0HdiskJQwg6wwphhODj3vk9ZOal0s0eQrHGlBUdEwDqmy3IcqLnNcuydES7QvfD8nI4hoGfs9UGatYCvkfK1HkFGQHPIY7iwBnaSJynCfcClkGCrB3v2-77zcmKNgudgBIQnXzZo-ETjUs1UhA9UXIIkyFBmGYy2oCGM1A+P9Cz4vq+b4sxnltIbCoXWzQiKUTcAoTg4AdAMAbBAcoVcGRVGjNGRoRMaAk0itFFyJ0mCUwSsJHE41JqOVrhggAEmNCaBAPLfxAWAiBAgoHBQALLQzIF0RBtFV5Q0MrDXiaD5gMKYaQLoFDgGgInjQjM5Q+GcNIAADVYR3DhMNSDpFkAQmKkjFHSKET-QqzNEBsJoAaNAhdKxlBLkwMufdv54xASgEMcskFIiMSNExydS7lxXjQB+AcRosAYKGMgU08jxnPCNEI6RrYH1jr4-xrAlBBNpswdIYSIluEoSWMoRRRAOPVmJRgod9KGWMjUcy9RbzmVmjUNojdrb5NQkZeYxT6ZlKgBUqpHV-aB0zrUgy9TGmlPKZUxuaJ5ZeM6fHEOttCkNJKXUZprTG7FBjq7cZHtJm9JmXMwZwlhlJOdsshOayikbIGW01JwD9olQUOk86-ABT6JErLcShz5jiwACzlPmuU15ABORu9tulTMqe8lpDQPm-MxoVNcbAeQOJQDyBcMBcBGxSndFiSMaBwvVGYJFzzLJ4sbpihFSLFacGVrxa880IUxCIkQwlohTJuBiHjWl8L6VuCIlc+yNysn3K3FixFCtkqkpVhZGIecbJ8qJQC+peLzJ1EbpK7FyLbqm3ul-PMRE4hmAcNkg2Tz1I9OMrKqySk9V5JJWS1WT0Gq4rlXKxuzz5pzNvM628WjNVIHAPwXVjy8mOpdeUgNLSXV-LNfLC1IrxWeX+f611ga3W6QNVM418qIWao3MLYQ9ycma2lUclpNR0rmQABynOjWGwVStI31XJnmkydqTU2VtRZe1abr7JFwPZPQiJs0VtjeUuobQSlm1Db68NQrLXTRrapZt+LE3N2TQ21NZzSCaviEkH1GtDbPO+UO6aNRgWzR+aOrd4kI28SjTQGNSaZVLodTe4yTr43xvde2mI6ASiaF7WOutT792zXMulYtJ7cnjqrRe6dL1Z2tqbQ++tLbG2kG-pquAZo4DVm-aev1cHC3AoPUW4NCanoVvPVa6NNq4MpvvQu29CHl1IfZW+ngdziM-v9ZZM2zTOMgdzaRqd1ra3QcQwYyjd623MBiBuRFm7QN1sqZZeTJSOg8cNnxy9ygKM0aNWJ2DWn4NzpXTENCl0v3HSTmdC6acqWa2M2nBxGDUJy1s6UdOhmu1gDgCWez5mRAEQ8yWKlBBU4ue811FOFIrOGeQgEcgzh7kOagNF4osXSBUpxLIGAKXQunVlBMTL+g0si2S3FszYWcRwGK6lwzKALpZfiz5mrlWqWanNPVsLLWqWrnjvZbLri3b2SpalKc+gWOeQS0NqIilDM8lM2NnzM3rM6giM+c8vXUJLZWwwazeDOJoA2ygVbbWcvkjIekPbtjNtUtDDOOrpWcvXdwClwLmW1uEEy9Zw2zmMN3eTrmr7rmGOkDTDvM7NyTOvYNLtsHkXAcpAIKiM7n3ochaO64nsCO-vI6urDzaPxC7pfy1mn7RV9B45GgTp7hnSUB2+3NsL7nPMA5oLgshqiXIoOwSuvGMQnLNh5ZXciRc8IEL51trnYhkgMHR-zpmguXGOWl2LwH3Ohh2FG1bOXxd766HPBClXHTQnBdp54g3ccE4JaCxFlzeuJcxEWYfJJRvZs0Ht7HLpFunc2-EDEUZhurdlCScLezpv3c+ctyZwPAOrG29d5xcPadI8ONj2bkOHv-ehOFl75IvuJJgBp378H8WQ8rMTmF6nnAA-x+t+L73yfy+V6d0nvZKes4+frwXmH0fvc56rwH7gKAMzB8fiNUPYXe821yBmLPdvm-j4kpP6A9zk+j5y3P-vU+a-Z9N3P9WhiYVF+Hy30vq+nfSyAnvqPbhufJ53+fguzu3tLKP2niPu-7-T5zzNof3jn-zcz5vmfJ-L-JfZvFfZOBbAAnvIPA-H-MApQfgS-YaGPWfaA7KUAkvC3f-ZXW3HPCAAARwSFbG-zGXNx83wMIJ4Gn2T3IKIJAKfzgPvgINbA-1N3b2IJHwwLbzzwr0QOv2bzYLoIdwYPbxYMPwmxG3YN-zC3EKm2wNr2bxkPV0fyEM4OkIpEmyVy7y30P1ASYGJFyCJzG2L1ILLxxH0IKwALrzMIMKb3oNUJy10PMKqzkO0J-3RgfxzwYPcKoOb3cNsJUJMJy28JXTXQalV0OwE1Unl21zV3E2SH+V5xxCUP+WiMSNEDiJSFrSl3hxlyvTCK12yNuQyISLyHiAfxSIKNKOx2QzfX+T8OzQag5yvHVSBxyggAiAcRpjpjaBiAekC3Kh0A4HuS6MxGYj6MMxylkC9GGI4kYlGJBRaKIkqDkWJnijZ1ilfWYBaBWOZ1IXwRrhik50ByWIgADh2IeSw3lmDEy2Th4VOM4BU3ElUGVhJ3hUgwyCaM-nRS8k4l+kXg03h1sSllCikThiUHGMBxmEoEWnuEeCqCAA` + ); +}); - // Expect text to be reset - await expect( - page.locator("text=before a colon creates a label").first() - ).toBeVisible(); - }); +test("Open mermaid.live link", async ({ page }) => { + await openExportDialog(page); - test("Creating a new chart from a template immediatetly creates a local chart", async ({ - page, - browserName, - }) => { - if (browserName === "firefox") { - // Firefox has a weird bug, most likely due to the "#" in the URL - return; - } - await page.goto(BASE_URL); - // go to url - await page.goto( - `${BASE_URL}/n#C4ewBARgpmCWB2ZgAsYBMQGMCuBbK8wAUALxllEDeRYYARAA4CGATgM5Qt0Bc9A5iyYNkAWg4AbKJlBciAX1LkSQA` - ); - - // expect "to be in the document" to be in the document - await expect( - page.locator("text=to be in the document").first() - ).toBeVisible(); - - // expect the url to contain "temp" in it - expect(page.url()).toContain("temp"); + const page1Promise = page.waitForEvent("popup"); + await page.getByTestId("Mermaid Live").click(); + const page1 = await page1Promise; + await expect(page1.getByText('["This app works by typing"]')).toBeVisible({ + timeout: 15 * 1000, }); +}); - test("Rename chart", async ({ page }) => { - // Click [aria-label="Rename"] - await page.locator('[aria-label="Rename"]').click(); - // Fill input[name="name"] - await page.locator('input[name="name"]').fill("my new chart"); - // Click button:has-text("Rename") - await page.locator('button:has-text("Rename")').click(); - await expect(page).toHaveURL(`${BASE_URL}/my-new-chart`); - // Click text=my-new-chart - await expect(page.locator("text=my-new-chart")).toBeVisible(); - // Click [aria-label="Rename"] - await page.locator('[aria-label="Rename"]').click(); - // Press a with modifiers - await page.locator('input[name="name"]').press("Meta+a"); - // Fill input[name="name"] - await page.locator('input[name="name"]').fill("cool chart"); - // Click button:has-text("Rename") - await page.locator('button:has-text("Rename")').click(); - await expect(page).toHaveURL(`${BASE_URL}/cool-chart`); - // Click text=cool-chart - await expect(page.locator("text=cool-chart")).toBeVisible(); - }); +test("Change Language", async ({ page }) => { + await goToTab(page, "Settings"); + // Click [aria-label="Select Language\: Deutsch"] + await page.locator('[aria-label="Select Language\\: Deutsch"]').click(); - test("Download PNG", async ({ page }) => { - await openExportDialog(page); - // Click [aria-label="Download PNG"] - const [download] = await Promise.all([ - page.waitForEvent("download"), - page.locator('[aria-label="Download PNG"]').click(), - ]); + // Expect to find a button with the text "Einstellungen" + await expect( + page.getByRole("heading", { name: "Einstellungen" }) + ).toBeVisible(); +}); - expect(download.suggestedFilename()).toBe("flowchart-fun.png"); - }); +test("Change Appearance", async ({ page }) => { + await goToTab(page, "Settings"); + await page.locator('[aria-label="Dark Mode"]').click(); + // get value of css custom property --color-background + const [background, foreground] = await page.evaluate(() => { + return [ + getComputedStyle(document.body).getPropertyValue("--color-background"), + getComputedStyle(document.body).getPropertyValue("--color-foreground"), + ]; + }); + expect(background.trim()).toBe("#0f0f0f"); + expect(foreground.trim()).toBe("rgb(250, 250, 250)"); +}); - test("Download JPG", async ({ page }) => { - await openExportDialog(page); - // Click [aria-label="Download JPG"] - const [download] = await Promise.all([ - page.waitForEvent("download"), - page.locator('[aria-label="Download JPG"]').click(), - ]); +test("Submit Feedback", async ({ page }) => { + // click button with text "Help" + await page.locator('button:has-text("Help")').click(); - expect(download.suggestedFilename()).toBe("flowchart-fun.jpg"); - }); + // click button with text "Feedback" + await page.locator('a:has-text("Feedback")').first().click(); - test("Copy Fullscreen Link", async ({ page }) => { - try { - await openExportDialog(page); - - // Expect input to have the correct value - await expect( - page.locator('text=FullscreenCopy >> input[type="text"]') - ).toHaveValue( - `${BASE_URL}/f#CoCwlgzgBAhgDnKB3A9gJwNbQEYE8oAuucYAdgOYBQUUAkqQCYCmpBZ5UAxmkzAU9BhQANmQyEUhEEy4BXND1YiyTarFL5+ADwIAuKNiYAzdDKGcUwlKS48+A2CJiHhagMJ3+j0aXEMwPJwEwviyEOxSMkxaMEFOLoTRBGo0ohgyBOAQ+gAUoJCwCMjoWAaaxOwAlClQANqcYQQoALZ0ACIAulDoNTR4sAz+FOpQAKQArABCtG0TbeoMUDxGTIqcEZl8vTTK6VKQuQ0QTa0zlVAA9BdQAJooslwwNjDCEJJhMuEUwkwAtD4yCzNZosAgQSgXABUlB6zVkwjYAMoQJBrHBlAAEjAAG4yIyyUgAQiggB4NwCR+5RIRcgA` - ); - - await Promise.all([ - // Expect Checkmark to be in view - expect(page.locator('[data-testid="Copied Fullscreen"]')).toMatch(""), - page.locator('[aria-label="Copy Fullscreen"]').click(), - ]); - } catch { - // Take Screenshot - await page.screenshot({ path: "ERROR.png" }); - } - }); + // Click [data-testid="message"] + await page.locator('[data-testid="message"]').click(); - test("Copy With-Editor Link", async ({ page }) => { - try { - await openExportDialog(page); - - // Expect input to have the correct value - await expect( - page.locator('text=FullscreenCopy >> input[type="text"]') - ).toHaveValue( - `${BASE_URL}/f#CoCwlgzgBAhgDnKB3A9gJwNbQEYE8oAuucYAdgOYBQUUAkqQCYCmpBZ5UAxmkzAU9BhQANmQyEUhEEy4BXND1YiyTarFL5+ADwIAuKNiYAzdDKGcUwlKS48+A2CJiHhagMJ3+j0aXEMwPJwEwviyEOxSMkxaMEFOLoTRBGo0ohgyBOAQ+gAUoJCwCMjoWAaaxOwAlClQANqcYQQoALZ0ACIAulDoNTR4sAz+FOpQAKQArABCtG0TbeoMUDxGTIqcEZl8vTTK6VKQuQ0QTa0zlVAA9BdQAJooslwwNjDCEJJhMuEUwkwAtD4yCzNZosAgQSgXABUlB6zVkwjYAMoQJBrHBlAAEjAAG4yIyyUgAQiggB4NwCR+5RIRcgA` - ); - - await Promise.all([ - // Expect Checkmark to be in view - expect( - page.locator('[data-testid="Copied With Editor"]') - ).toBeVisible(), - page.locator('[aria-label="Copy With Editor"]').click(), - ]); - } catch { - // Take Screenshot - await page.screenshot({ path: "ERROR.png" }); - } - }); + // Fill [data-testid="message"] + await page.locator('[data-testid="message"]').fill("This is a test"); - test("Copy Mermaid JS Code", async ({ page }) => { - try { - await openExportDialog(page); - - await Promise.all([ - expect( - page.locator('[data-testid="Copied Mermaid Code"]') - ).toBeVisible(), - page.locator('[aria-label="Copy Mermaid Code"]').click(), - ]); - } catch { - // Take Screenshot - await page.screenshot({ path: "ERROR.png" }); - } - }); + // Click [data-testid="email"] + await page.locator('[data-testid="email"]').click(); + // Fill [data-testid="email"] + await page.locator('[data-testid="email"]').fill("test@test.com"); - test("Open mermaid.live link", async ({ page }) => { - await openExportDialog(page); + await page.locator('button:has-text("Submit")').click(); + // Click text=Thank you for your feedback! + await expect(page.locator("text=Thank you for your feedback!")).toBeVisible(); +}); - const page1Promise = page.waitForEvent("popup"); - await page.getByTestId("Mermaid Live").click(); - const page1 = await page1Promise; - await expect(page1.getByText('["This app works by typing"]')).toBeVisible({ - timeout: 15 * 1000, +test("Manipulate Editor Code", async ({ page }) => { + // Type in editor + + // Click text=This app works by typing >> nth=0 + await page.locator("text=This app works by typing").first().click(); + // Press a with modifiers + await page + .locator( + '[aria-label="Editor content\\;Press Alt\\+F1 for Accessibility Options\\."]' + ) + .press("Meta+a"); + await page + .locator( + '[aria-label="Editor content\\;Press Alt\\+F1 for Accessibility Options\\."]' + ) + .type("hello world"); + + await expect( + page.locator('div[role="code"] >> text=hello world') + ).toBeVisible(); + + // Resize Editor/Graph + + await page.getByTestId("Editor Tab: Layout").click(); + + // Contract Graph + + // Expand Graph + + // Change Graph Options Layout Direction + await page + .locator('button[role="combobox"]:has-text("Top to Bottom")') + .click(); + await page.locator('div[role="option"]:has-text("Left to Right")').click(); + + // Change Graph Options Layout + await page.locator('button[role="combobox"]:has-text("Dagre")').click(); + await page.locator('div[role="option"]:has-text("Klay")').click(); + + // Right Click on Graph & Download PNG + await page + .locator("#cy canvas") + .first() + .click({ + button: "right", + position: { + x: 440, + y: 74, + }, }); - }); - - test("Change Language", async ({ page }) => { - await goToTab(page, "Settings"); - // Click [aria-label="Select Language\: Deutsch"] - await page.locator('[aria-label="Select Language\\: Deutsch"]').click(); - - // Expect to find a button with the text "Einstellungen" - await expect( - page.getByRole("heading", { name: "Einstellungen" }) - ).toBeVisible(); - }); - - test("Change Appearance", async ({ page }) => { - await goToTab(page, "Settings"); - await page.locator('[aria-label="Dark Mode"]').click(); - // get value of css custom property --color-background - const [background, foreground] = await page.evaluate(() => { - return [ - getComputedStyle(document.body).getPropertyValue("--color-background"), - getComputedStyle(document.body).getPropertyValue("--color-foreground"), - ]; + // Click text=Download PNG + const [png] = await Promise.all([ + page.waitForEvent("download"), + page.locator("text=Download PNG").click(), + ]); + + expect(png.suggestedFilename()).toBe("flowchart-fun.png"); + + // Right Click on Graph & Download JPG + await page + .locator("#cy canvas") + .first() + .click({ + button: "right", + position: { + x: 267, + y: 297, + }, }); - expect(background.trim()).toBe("#0f0f0f"); - expect(foreground.trim()).toBe("rgb(250, 250, 250)"); - }); - - test("Submit Feedback", async ({ page }) => { - // click button with text "Help" - await page.locator('button:has-text("Help")').click(); - - // click button with text "Feedback" - await page.locator('a:has-text("Feedback")').first().click(); - - // Click [data-testid="message"] - await page.locator('[data-testid="message"]').click(); - - // Fill [data-testid="message"] - await page.locator('[data-testid="message"]').fill("This is a test"); + // Click text=Download JPG + const [jpg] = await Promise.all([ + page.waitForEvent("download"), + page.locator("text=Download JPG").click(), + ]); - // Click [data-testid="email"] - await page.locator('[data-testid="email"]').click(); - // Fill [data-testid="email"] - await page.locator('[data-testid="email"]').fill("test@test.com"); + expect(jpg.suggestedFilename()).toBe("flowchart-fun.jpg"); +}); - await page.locator('button:has-text("Submit")').click(); - // Click text=Thank you for your feedback! - await expect( - page.locator("text=Thank you for your feedback!") - ).toBeVisible(); - }); +test("Export to Visio CSV", async ({ page }) => { + await openExportDialog(page); + await page.getByRole("tab", { name: "Visio" }).click(); + const [download] = await Promise.all([ + page.waitForEvent("download"), + page.getByTestId("Visio Flowchart").click(), + ]); + expect(download.suggestedFilename()).toBe("flowchart-fun-visio-flow.csv"); + const [download1] = await Promise.all([ + page.waitForEvent("download"), + page.getByTestId("Visio Org Chart").click(), + ]); + expect(download1.suggestedFilename()).toBe("flowchart-fun-visio-org.csv"); +}); - test("Manipulate Editor Code", async ({ page }) => { - try { - // Type in editor - - // Click text=This app works by typing >> nth=0 - await page.locator("text=This app works by typing").first().click(); - // Press a with modifiers - await page - .locator( - '[aria-label="Editor content\\;Press Alt\\+F1 for Accessibility Options\\."]' - ) - .press("Meta+a"); - await page - .locator( - '[aria-label="Editor content\\;Press Alt\\+F1 for Accessibility Options\\."]' - ) - .type("hello world"); - - await expect( - page.locator('div[role="code"] >> text=hello world') - ).toBeVisible(); - - // Resize Editor/Graph - - await page.getByTestId("Editor Tab: Layout").click(); - - // Contract Graph - - // Expand Graph - - // Change Graph Options Layout Direction - await page - .locator('button[role="combobox"]:has-text("Top to Bottom")') - .click(); - await page - .locator('div[role="option"]:has-text("Left to Right")') - .click(); - - // Change Graph Options Layout - await page.locator('button[role="combobox"]:has-text("Dagre")').click(); - await page.locator('div[role="option"]:has-text("Klay")').click(); - - // Change Graph Options Theme - await page.getByTestId("Editor Tab: Style").click(); - await page.locator('button[role="combobox"]:has-text("Light")').click(); - await page.locator('div[role="option"]:has-text("Dark")').click(); - - // Right Click on Graph & Download PNG - await page - .locator("#cy canvas") - .first() - .click({ - button: "right", - position: { - x: 440, - y: 74, - }, - }); - // Click text=Download PNG - const [png] = await Promise.all([ - page.waitForEvent("download"), - page.locator("text=Download PNG").click(), - ]); - - expect(png.suggestedFilename()).toBe("flowchart.png"); - - // Right Click on Graph & Download JPG - await page - .locator("#cy canvas") - .first() - .click({ - button: "right", - position: { - x: 267, - y: 297, - }, - }); - // Click text=Download JPG - const [jpg] = await Promise.all([ - page.waitForEvent("download"), - page.locator("text=Download JPG").click(), - ]); - - expect(jpg.suggestedFilename()).toBe("flowchart.jpg"); - } catch { - // Take Screenshot - await page.screenshot({ path: "ERROR.png" }); - } - }); +test("Can follow Import Data to pricing page", async ({ page }) => { + await page.getByRole("button", { name: "Import Data" }).click(); - test("Export to Visio CSV", async ({ page }) => { - await openExportDialog(page); - await page.getByRole("tab", { name: "Visio" }).click(); - const [download] = await Promise.all([ - page.waitForEvent("download"), - page.getByTestId("Visio Flowchart").click(), - ]); - expect(download.suggestedFilename()).toBe("flowchart-fun-visio-flow.csv"); - const [download1] = await Promise.all([ - page.waitForEvent("download"), - page.getByTestId("Visio Org Chart").click(), - ]); - expect(download1.suggestedFilename()).toBe("flowchart-fun-visio-org.csv"); - }); + // Click on getByRole('button', { name: 'Learn More' }) + await page.getByRole("button", { name: "Learn More" }).click(); - test("Can follow Import Data to pricing page", async ({ page }) => { - await page.getByRole("button", { name: "Import Data" }).click(); - await page.getByRole("link", { name: "Learn More" }).click(); - // expect to be on /pricing page - expect(new URL(page.url()).pathname).toBe("/pricing"); - }); + // expect to be on /pricing page + expect(new URL(page.url()).pathname).toBe("/pricing"); }); diff --git a/app/e2e/utils.ts b/app/e2e/utils.ts index 66ef2fac..c067e71a 100644 --- a/app/e2e/utils.ts +++ b/app/e2e/utils.ts @@ -1,5 +1,4 @@ import { Page } from "@playwright/test"; -import { createClient } from "@supabase/supabase-js"; import axios from "axios"; import crypto from "crypto"; import Stripe from "stripe"; @@ -10,20 +9,10 @@ if (!rapidAPIKey) throw new Error("RAPID_API_KEY not set"); const stripeKey = process.env.STRIPE_KEY_TEST_ENV; if (!stripeKey) throw new Error("STRIPE_KEY_TEST_ENV not set"); -// SUPABASE_TEST_URL -const supabaseTestUrl = process.env.SUPABASE_TEST_URL; -if (!supabaseTestUrl) throw new Error("SUPABASE_TEST_URL not set"); - -// SUPABASE_TEST_ANON_KEY -const supabaseTestAnonKey = process.env.SUPABASE_TEST_ANON_KEY; -if (!supabaseTestAnonKey) throw new Error("SUPABASE_TEST_ANON_KEY not set"); - const stripe = new Stripe(stripeKey, { - apiVersion: "2020-08-27", + apiVersion: "2022-11-15", }); -const _supabase = createClient(supabaseTestUrl, supabaseTestAnonKey); - export const BASE_URL = process.env.E2E_START_URL ?? "http://localhost:3000"; const EMAIL_DOMAINS_LIST: string[] = []; diff --git a/app/package.json b/app/package.json index 5e041bb5..3e3dfa84 100644 --- a/app/package.json +++ b/app/package.json @@ -16,13 +16,13 @@ "extract": "lingui extract --clean", "compile": "lingui compile", "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx src --color --fix", - "check:types": "tsc --noEmit", + "typecheck": "tsc --noEmit", "format": "prettier --write '{src,e2e}/**/*.{ts,tsx,scss,css,json}'", "e2e:env": "export $(cat .env.e2e | xargs)", "e2e": "playwright test --config=playwright.config.ts", - "e2e:debug": "DEBUG=1 pnpm run e2e --workers 1", + "e2e:debug": "DEBUG=1 pnpm run e2e --workers 1 --ui", "e2e:generate": "npx playwright codegen localhost:3000", - "generate:types": "export $(cat .env.local | xargs) && supabase gen types typescript --db-url \"${DB_URL}\" > src/types/database.types.ts", + "generate:types": "export $(cat .env.local | xargs) && supabase gen types typescript --project-id \"${PROJECT_ID}\" > src/types/database.types.ts", "analyze": "source-map-explorer 'build/static/js/*.js'", "autotranslations": "node scripts/autotranslations.mjs" }, @@ -30,21 +30,21 @@ "@formkit/auto-animate": "1.0.0-beta.6", "@lingui/core": "^3.8.9", "@lingui/react": "^3.8.9", - "@monaco-editor/react": "^4.4.6", + "@monaco-editor/react": "^4.5.1", "@notionhq/client": "^0.4.13", - "@radix-ui/react-alert-dialog": "^1.0.2", - "@radix-ui/react-collapsible": "^1.0.2", - "@radix-ui/react-dialog": "^1.0.3", - "@radix-ui/react-dropdown-menu": "^2.0.1", - "@radix-ui/react-hover-card": "^0.1.5", - "@radix-ui/react-navigation-menu": "^1.0.0", - "@radix-ui/react-popover": "^1.0.3", - "@radix-ui/react-progress": "^1.0.2", - "@radix-ui/react-radio-group": "^1.1.2", - "@radix-ui/react-select": "^1.2.1", - "@radix-ui/react-slider": "^1.1.0", - "@radix-ui/react-tabs": "^1.0.1", - "@radix-ui/react-toggle": "^0.1.4", + "@radix-ui/react-alert-dialog": "^1.0.4", + "@radix-ui/react-collapsible": "^1.0.3", + "@radix-ui/react-dialog": "^1.0.4", + "@radix-ui/react-dropdown-menu": "^2.0.5", + "@radix-ui/react-hover-card": "^1.0.6", + "@radix-ui/react-navigation-menu": "^1.1.3", + "@radix-ui/react-popover": "^1.0.6", + "@radix-ui/react-progress": "^1.0.3", + "@radix-ui/react-radio-group": "^1.1.3", + "@radix-ui/react-select": "^1.2.2", + "@radix-ui/react-slider": "^1.1.2", + "@radix-ui/react-tabs": "^1.0.4", + "@radix-ui/react-toggle": "^1.0.3", "@radix-ui/react-tooltip": "^1.0.6", "@react-hook/throttle": "^2.2.0", "@sendgrid/mail": "^7.4.6", @@ -52,16 +52,10 @@ "@sentry/tracing": "^7.38.0", "@stripe/react-stripe-js": "^2.1.0", "@stripe/stripe-js": "^1.52.1", - "@supabase/gotrue-js": "^2", - "@supabase/supabase-js": "^2", + "@supabase/supabase-js": "^2.31.0", "@svgr/webpack": "^6.3.1", - "@testing-library/jest-dom": "^5.11.4", - "@testing-library/react": "^11.1.0", - "@testing-library/user-event": "^12.1.10", "@tone-row/slang": "^1.2.35", "@tone-row/strip-comments": "^2.0.1", - "@types/react": "^17.0.0", - "@types/react-dom": "^17.0.0", "buffer": "^6.0.3", "classnames": "^2.3.2", "core-js": "^3.18.1", @@ -75,7 +69,7 @@ "deepmerge": "^4.2.2", "eslint": "^8.3.0", "file-saver": "^2.0.5", - "framer-motion": "^4.1.17", + "framer-motion": "^10.13.1", "graph-selector": "^0.9.10", "gray-matter": "^4.0.2", "highlight.js": "^11.7.0", @@ -83,11 +77,10 @@ "js-base64": "^3.7.5", "js-cookie": "^3.0.5", "lodash.throttle": "^4.1.1", - "logsnag": "^0.1.6", "lz-string": "^1.4.4", "make-plural": "^7.1.0", "marked": "^4.1.1", - "monaco-editor": "^0.34.0", + "monaco-editor": "0.33.0", "moniker": "^0.1.2", "notion-to-md": "^2.5.5", "openai": "^3.2.1", @@ -97,17 +90,17 @@ "postcss-flexbugs-fixes": "^5.0.2", "prettier": "^2.3.1", "re-resizable": "^6.9.0", - "react": "^17.0.1", + "react": "^18.2.0", "react-confetti": "^6.1.0", "react-contexify": "^5.0.0", - "react-dom": "^17.0.1", + "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", "react-ga": "^3.3.0", "react-gtm-module": "^2.0.11", "react-hook-form": "^7.0.7", "react-icons": "^4.9.0", "react-query": "^3.26.0", - "react-router-dom": "^5.2.0", + "react-router-dom": "^6.14.2", "react-scripts": "5.0.1", "react-use-localstorage": "^3.5.3", "shared": "workspace:*", @@ -137,10 +130,12 @@ ] }, "devDependencies": { - "@lingui/cli": "^3.13.2", + "@lingui/cli": "^3.17.2", "@lingui/macro": "^3.13.2", - "@playwright/test": "1.29.0", - "@testing-library/react-hooks": "^8.0.1", + "@playwright/test": "1.36.2", + "@testing-library/jest-dom": "^5.17.0", + "@testing-library/react": "^14.0.0", + "@testing-library/user-event": "^14.4.3", "@types/css-font-loading-module": "^0.0.7", "@types/cytoscape": "^3.19.9", "@types/download": "^6.2.4", @@ -156,6 +151,8 @@ "@types/node-localstorage": "^1.3.0", "@types/pako": "^2.0.0", "@types/papaparse": "^5.3.7", + "@types/react": "^18.2.15", + "@types/react-dom": "^18.2.7", "@types/react-gtm-module": "^2.0.1", "@types/react-router-dom": "^5.1.7", "@types/react-select": "^4.0.17", @@ -163,8 +160,8 @@ "@types/svg-parser": "^2.0.1", "@types/svgo": "^2.6.0", "@types/testing-library__jest-dom": "^5.14.5", - "@typescript-eslint/eslint-plugin": "^5", - "@typescript-eslint/parser": "^5", + "@typescript-eslint/eslint-plugin": "^6.2.0", + "@typescript-eslint/parser": "^6.2.0", "axios": "^0.27.2", "concurrently": "^6.0.0", "dotenv": "^16.0.1", @@ -175,18 +172,21 @@ "eslint-plugin-prettier": "^3.4.0", "eslint-plugin-react": "^7.24.0", "eslint-plugin-react-hooks": "^4.2.0", + "eslint-plugin-react-refresh": "^0.4.3", "eslint-plugin-simple-import-sort": "^7.0.0", "history": "^5.3.0", "identity-obj-proxy": "^3.0.0", "jest-canvas-mock": "^2.3.1", "jsdom": "^20.0.3", "minimist": "^1.2.5", - "msw": "^0.36.8", + "msw": "^1.2.3", + "react@latest": "link:@@testing-library/react@latest", "source-map-explorer": "^2.5.2", + "supabase": "^1.82.1", "tailwindcss": "^3.2.6", "ts-node": "^10.9.1", "ts-unused-exports": "^7.0.3", - "typescript": "^4.8.4", + "typescript": "^5.1.6", "web-worker": "^1.2.0" }, "jest": { @@ -198,5 +198,8 @@ "\"node_modules/.pnpm/(?!react-use-localstorage)/\"", "\"node_modules/.pnpm/(?!(monaco-editor|monaco-editor-core)/)\"" ] + }, + "overrides": { + "fork-ts-checker-webpack-plugin": "^6.5.3" } } diff --git a/app/playwright.config.ts b/app/playwright.config.ts index 640c4848..fe6debd2 100644 --- a/app/playwright.config.ts +++ b/app/playwright.config.ts @@ -22,7 +22,7 @@ const config: PlaywrightTestConfig = { height: 1080, }, // Development - headless: isDebug ? false : true, + // headless: isDebug ? false : true, launchOptions: { slowMo: isDebug ? 500 : 0, }, diff --git a/app/scripts/autotranslations.mjs b/app/scripts/autotranslations.mjs index 93a3dc69..d2b8163f 100644 --- a/app/scripts/autotranslations.mjs +++ b/app/scripts/autotranslations.mjs @@ -121,7 +121,7 @@ for (const locale of locales) { // while there are still phrases to translate while (phrases.length > 0) { - // pop off the first 6 phrases + /** pop off the first 6 phrases */ const batch = phrases.splice(0, 6); const prompt = `Translate phrases from en to ${locale}\n\nEN\n${batch @@ -130,7 +130,8 @@ for (const locale of locales) { // we will retry up to 3 times to get the right number of translations let retries = 3; - // @type {string[]} + + /** @type {string[]} */ let translations = []; while (retries > 0 && translations.length !== batch.length) { @@ -155,10 +156,18 @@ for (const locale of locales) { // add the translations to the final phrases finalPhrases = finalPhrases.concat( - batch.map((phrase, index) => ({ - ...phrase, - translation: translations[index].slice(2), - })) + batch.map((phrase, index) => { + if (translations[index]) { + return { + ...phrase, + translation: translations[index].slice(2), + }; + } + console.log( + `Inspect this phrase: ${phrase.text}\nin this language: ${locale}` + ); + return phrase; + }) ); console.log(`${finalPhrases.length}/${totalPhrases} translated.`); diff --git a/app/src/components/AppContext.tsx b/app/src/components/AppContext.tsx index fbe7a11b..55dc08b4 100644 --- a/app/src/components/AppContext.tsx +++ b/app/src/components/AppContext.tsx @@ -1,4 +1,4 @@ -import { Session } from "@supabase/gotrue-js"; +import { Session } from "@supabase/supabase-js"; import { createContext, Dispatch, @@ -8,7 +8,6 @@ import { useContext, useEffect, useMemo, - useRef, useState, } from "react"; import { useLocation } from "react-router-dom"; @@ -16,12 +15,10 @@ import useLocalStorage from "react-use-localstorage"; import Stripe from "stripe"; import { LOCAL_STORAGE_SETTINGS_KEY } from "../lib/constants"; -import { loadSponsorOnlyLayouts } from "../lib/cytoscape"; import { useCustomerInfo, useHostedCharts } from "../lib/queries"; -import { supabase } from "../lib/supabaseClient"; -import { useGraphStore } from "../lib/useGraphStore"; import { languages } from "../locales/i18n"; import { colors, darkTheme } from "../slang/config"; +import { supabase } from "../lib/supabaseClient"; type Theme = typeof colors; @@ -114,38 +111,25 @@ const Provider = ({ children }: { children?: ReactNode }) => { const [checkedSession, setCheckedSession] = useState(false); const [session, setSession] = useState(null); - const sponsorLayoutsLoading = useRef(false); - - /* Load Sponsor-only layouts when logged in */ - useEffect(() => { - // If not logged in, return - if (!session) return; - // If already loaded, return - if (useGraphStore.getState().sponsorLayoutsLoaded) return; - // If in the process of loading, return - if (sponsorLayoutsLoading.current) return; - sponsorLayoutsLoading.current = true; - loadSponsorOnlyLayouts().then(() => { - useGraphStore.setState({ sponsorLayoutsLoaded: true }); - }); - }, [session]); useEffect(() => { - if (!supabase) { - setCheckedSession(true); - return; - } + requestAnimationFrame(() => { + if (!supabase) { + setCheckedSession(true); + return; + } - (async () => { - const { - data: { session }, - } = await supabase.auth.getSession(); - setSession(session); - setCheckedSession(true); - supabase.auth.onAuthStateChange((_event, session) => { + (async () => { + const { + data: { session }, + } = await supabase.auth.getSession(); setSession(session); - }); - })(); + setCheckedSession(true); + supabase.auth.onAuthStateChange((_event, session) => { + setSession(session); + }); + })(); + }); }, []); // Close Share Modal when navigating diff --git a/app/src/components/AuthWall.tsx b/app/src/components/AuthWall.tsx new file mode 100644 index 00000000..2e74f087 --- /dev/null +++ b/app/src/components/AuthWall.tsx @@ -0,0 +1,69 @@ +import { ReactNode, Suspense, useContext, useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { AppContext } from "./AppContext"; +import Spinner from "./Spinner"; + +/** + * This component redirects to the login page if the user is not logged in. + */ +export function AuthWall({ children }: { children: ReactNode }) { + const { checkedSession, session } = useContext(AppContext); + const [isLoading, setIsLoading] = useState(true); + const navigate = useNavigate(); + + useEffect(() => { + if (!checkedSession) return; + if (!session) { + // create params for login page + const params = new URLSearchParams(); + // show auth wall at top of page + params.set("showAuthWallWarning", "true"); + + // full redirect including hash and search params + params.set("redirectUrl", getAuthSafeUrl()); + + // go to login page with params + const fullPath = `/l?${params.toString()}`; + + // replace the state so back button doesn't infinite loop + navigate(fullPath, { + replace: true, + }); + } else { + setIsLoading(false); + } + }, [checkedSession, navigate, session]); + + return ( + }> + {isLoading ? : children} + + ); +} + +function Loading() { + return ( +
+ +
+ ); +} + +/** + * Takes the current url + * If the pathname is "/n" + * and there is a hash + * It moves the hash to the pathname "/n/:hash" + * Otherwise it returns the current url + * + * This is needed because logging in will wipe the hash (access_token) + * so we can't rely on it for generating templates + */ +function getAuthSafeUrl() { + const url = new URL(window.location.href); + if (url.pathname === "/n" && url.hash) { + url.pathname = `/n/${url.hash.slice(1)}`; + url.hash = ""; + } + return url.toString(); +} diff --git a/app/src/components/CloneButton.tsx b/app/src/components/CloneButton.tsx index 30b95319..74bb39b7 100644 --- a/app/src/components/CloneButton.tsx +++ b/app/src/components/CloneButton.tsx @@ -1,24 +1,31 @@ import { Trans } from "@lingui/macro"; import { CopySimple } from "phosphor-react"; -import { useHistory } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; -import { randomChartName, titleToLocalStorageKey } from "../lib/helpers"; +import { slugify, titleToLocalStorageKey } from "../lib/helpers"; import { docToString, useDoc } from "../lib/useDoc"; import { Button2 } from "../ui/Shared"; +import { useContext, useState } from "react"; +import { AppContext } from "./AppContext"; +import { getFunFlowchartName } from "../lib/getFunFlowchartName"; +import { languages } from "../locales/i18n"; export function CloneButton() { - const { push } = useHistory(); + const navigate = useNavigate(); const fullText = useDoc((s) => docToString(s)); + const language = useContext(AppContext).language; + const [name] = useState( + slugify(getFunFlowchartName(language as keyof typeof languages)) + ); return ( { - const newChartTitle = randomChartName(); window.localStorage.setItem( - titleToLocalStorageKey(newChartTitle), + titleToLocalStorageKey(name), fullText ?? "" ); - push(`/${newChartTitle}`); + navigate(`/${name}`); }} rightIcon={} > diff --git a/app/src/components/ColorMode.tsx b/app/src/components/ColorMode.tsx index 3f26fa14..f170ab35 100644 --- a/app/src/components/ColorMode.tsx +++ b/app/src/components/ColorMode.tsx @@ -7,14 +7,18 @@ import { AppContext } from "./AppContext"; export default function ColorMode() { const { theme } = useContext(AppContext); - return createPortal( -