|
| 1 | +// Google per-service picker fan-out: the add-Google flow lets a user check |
| 2 | +// several Google products and submit them in one action; each checked preset |
| 3 | +// becomes its OWN integration (google_calendar, google_gmail, google_drive), |
| 4 | +// not one bundled "google" source. This drives the whole browser path: |
| 5 | +// |
| 6 | +// 1. Open /integrations/add/google (the Google-owned add flow). |
| 7 | +// 2. Clear the featured defaults, then check exactly Calendar + Gmail + Drive. |
| 8 | +// 3. Submit via `google-add-submit`. The flow fetches each preset's real |
| 9 | +// Google Discovery document (www.googleapis.com) and registers one |
| 10 | +// integration per product. |
| 11 | +// 4. The result panel shows three `add-result-row-*` rows, each data-state |
| 12 | +// "added", each with an Open link. |
| 13 | +// 5. Follow one Open link: the integration detail page shows the per-service |
| 14 | +// name ("Google Calendar"), proving the fan-out kept preset identities. |
| 15 | +// 6. Back on the integrations list, all three separate integrations exist |
| 16 | +// (asserted by their distinct slugs in the list UI). |
| 17 | +// 7. Re-open the add flow, check Calendar again, submit: its row now reports |
| 18 | +// data-state "skipped" (the integration already exists), proving the |
| 19 | +// idempotent add path. |
| 20 | +// |
| 21 | +// OUTBOUND DISCOVERY: unlike the emulator-backed scenarios, this exercises the |
| 22 | +// real add path. The Google preset add flow HARDCODES the Discovery host |
| 23 | +// (`normalizeGoogleDiscoveryUrl` only accepts googleapis.com HTTPS Discovery |
| 24 | +// endpoints) and the OAuth authorize/token URLs (accounts.google.com / |
| 25 | +// oauth2.googleapis.com). There is no baseUrl or custom-URL override that |
| 26 | +// redirects a preset's Discovery fetch at the emulator, so this scenario adds |
| 27 | +// the integrations against real Google Discovery documents (read-only, no |
| 28 | +// credentials, no OAuth). The cloud e2e runners (and CI's blacksmith runners) |
| 29 | +// have outbound internet, so the fetch resolves; the scenario asserts only on |
| 30 | +// the product's own catalog state, never on Google account data. |
| 31 | +import { expect } from "@effect/vitest"; |
| 32 | +import { Effect } from "effect"; |
| 33 | + |
| 34 | +import { scenario } from "../src/scenario"; |
| 35 | +import { Browser, Target } from "../src/services"; |
| 36 | +import { setPresetChecked } from "./support/picker"; |
| 37 | + |
| 38 | +scenario( |
| 39 | + "Google · the per-service picker fans out to separate integrations and skips existing ones", |
| 40 | + { timeout: 240_000 }, |
| 41 | + Effect.gen(function* () { |
| 42 | + const target = yield* Target; |
| 43 | + const browser = yield* Browser; |
| 44 | + const identity = yield* target.newIdentity(); |
| 45 | + |
| 46 | + yield* browser.session(identity, async ({ page, step }) => { |
| 47 | + await step("Open the Google add flow", async () => { |
| 48 | + await page.goto("/integrations/add/google", { waitUntil: "domcontentloaded" }); |
| 49 | + await page.getByRole("heading", { name: "Add Google integration" }).waitFor(); |
| 50 | + await page.getByText("Customize your Google connection").waitFor(); |
| 51 | + // The picker mounts with the featured defaults checked. |
| 52 | + await page.getByTestId("preset-checkbox-google-calendar").waitFor(); |
| 53 | + }); |
| 54 | + |
| 55 | + await step("Clear the defaults, then check exactly Calendar + Gmail + Drive", async () => { |
| 56 | + // Clear every featured default, then select only the three under test. |
| 57 | + const featuredDefaults = [ |
| 58 | + "google-calendar", |
| 59 | + "google-gmail", |
| 60 | + "google-sheets", |
| 61 | + "google-drive", |
| 62 | + "google-docs", |
| 63 | + ]; |
| 64 | + for (const presetId of featuredDefaults) { |
| 65 | + await setPresetChecked(page, presetId, false); |
| 66 | + } |
| 67 | + |
| 68 | + for (const presetId of ["google-calendar", "google-gmail", "google-drive"]) { |
| 69 | + await setPresetChecked(page, presetId, true); |
| 70 | + } |
| 71 | + // Everything else is unchecked: this is a scoped three-product selection. |
| 72 | + expect(await page.getByTestId("preset-checkbox-google-sheets").isChecked()).toBe(false); |
| 73 | + expect(await page.getByTestId("preset-checkbox-google-docs").isChecked()).toBe(false); |
| 74 | + }); |
| 75 | + |
| 76 | + await step("Submit the fan-out and see three added integrations", async () => { |
| 77 | + await page.getByTestId("google-add-submit").click(); |
| 78 | + // The result panel appears once every product is registered. Discovery |
| 79 | + // fetch + spec compile per product; allow generous time. |
| 80 | + const results = page.getByTestId("google-add-results"); |
| 81 | + await results.waitFor({ timeout: 120_000 }); |
| 82 | + |
| 83 | + for (const presetId of ["google-calendar", "google-gmail", "google-drive"]) { |
| 84 | + const row = page.getByTestId(`add-result-row-${presetId}`); |
| 85 | + await row.waitFor({ timeout: 120_000 }); |
| 86 | + expect( |
| 87 | + await row.getAttribute("data-state"), |
| 88 | + `${presetId} added as its own integration`, |
| 89 | + ).toBe("added"); |
| 90 | + // Each added row links out to its own integration page. |
| 91 | + await row.getByRole("link", { name: "Open" }).waitFor(); |
| 92 | + } |
| 93 | + }); |
| 94 | + |
| 95 | + await step("Open one product: its integration page shows the per-service name", async () => { |
| 96 | + await page |
| 97 | + .getByTestId("add-result-row-google-calendar") |
| 98 | + .getByRole("link", { name: "Open" }) |
| 99 | + .click(); |
| 100 | + await page.waitForURL(/\/integrations\/google_calendar\b/); |
| 101 | + // The detail header renders the per-service name, not a generic "Google". |
| 102 | + await page.getByText("Google Calendar").first().waitFor({ timeout: 20_000 }); |
| 103 | + }); |
| 104 | + |
| 105 | + await step("The integrations list has three separate Google integrations", async () => { |
| 106 | + await page.goto("/integrations", { waitUntil: "networkidle" }); |
| 107 | + // Each fanned-out integration is its own list entry, keyed by its slug |
| 108 | + // (the list renders the slug as each entry's description). |
| 109 | + for (const slug of ["google_calendar", "google_gmail", "google_drive"]) { |
| 110 | + await page.getByText(slug, { exact: true }).first().waitFor({ timeout: 20_000 }); |
| 111 | + } |
| 112 | + }); |
| 113 | + |
| 114 | + await step("Re-adding an existing product reports it as skipped", async () => { |
| 115 | + await page.goto("/integrations/add/google", { waitUntil: "domcontentloaded" }); |
| 116 | + await page.getByText("Customize your Google connection").waitFor(); |
| 117 | + |
| 118 | + // Clear the featured defaults so only Calendar (already added) is checked. |
| 119 | + for (const presetId of ["google-gmail", "google-sheets", "google-drive", "google-docs"]) { |
| 120 | + await setPresetChecked(page, presetId, false); |
| 121 | + } |
| 122 | + await setPresetChecked(page, "google-calendar", true); |
| 123 | + |
| 124 | + await page.getByTestId("google-add-submit").click(); |
| 125 | + const row = page.getByTestId("add-result-row-google-calendar"); |
| 126 | + await row.waitFor({ timeout: 120_000 }); |
| 127 | + expect( |
| 128 | + await row.getAttribute("data-state"), |
| 129 | + "an already-added product is skipped, not re-added or failed", |
| 130 | + ).toBe("skipped"); |
| 131 | + }); |
| 132 | + }); |
| 133 | + }), |
| 134 | +); |
0 commit comments