| | @@ -123,16 +123,29 @@ |
| 123 | 123 | ); |
| 124 | 124 | // The browser the editor opens: record the URL, as a person's browser would receive it. |
| 125 | 125 | const opened = join(work, "opened.txt"); |
| 126 | | -const browserScript = join(work, "browser.sh"); |
| 126 | +// Electron opens links with xdg-open on Linux, which hands them to $BROWSER when there's no desktop session. |
| 127 | +const bin = join(work, "bin"); |
| 128 | +mkdirSync(bin); |
| 129 | +const browserScript = join(bin, "xdg-open"); |
| 127 | 130 | writeFileSync(browserScript, `#!/bin/sh\necho "$1" >> ${opened}\n`); |
| 128 | 131 | chmodSync(browserScript, 0o755); |
| 129 | | -const env = { ...process.env, HOME: home, BROWSER: browserScript, XDG_CURRENT_DESKTOP: "", DONT_PROMPT_WSL_INSTALL: "1" }; |
| 130 | | -const common = ["--user-data-dir", userData, "--extensions-dir", exts, "--no-sandbox", "--disable-gpu"]; |
| 132 | +const env = { ...process.env, HOME: home, PATH: `${bin}:${process.env.PATH}`, BROWSER: browserScript, XDG_CURRENT_DESKTOP: "", DONT_PROMPT_WSL_INSTALL: "1" }; |
| 133 | +// No OS keyring in a test container: the editor keeps secrets with its own weaker encryption instead. |
| 134 | +const common = ["--user-data-dir", userData, "--extensions-dir", exts, "--no-sandbox", "--disable-gpu", "--password-store=basic"]; |
| 131 | 135 | execFileSync(CLI, [...common, "--install-extension", VSIX, "--force"], { env, stdio: "pipe", timeout: 120_000 }); |
| 132 | 136 | console.log("extension installed"); |
| 133 | 137 | |
| 134 | 138 | const app = await _electron.launch({ executablePath: EDITOR, args: [...common, "--disable-workspace-trust", "--skip-welcome", "--skip-release-notes", "--new-window"], env, timeout: 120_000 }); |
| 135 | 139 | const win = await app.firstWindow(); |
| 140 | +// On a failure, a screenshot of the editor as it was. |
| 141 | +async function fail(e) { |
| 142 | + if (SHOTS) await win.screenshot({ path: join(SHOTS, "failed.png") }).catch(() => {}); |
| 143 | + console.error(e); |
| 144 | + cleanup(); |
| 145 | + process.exit(1); |
| 146 | +} |
| 147 | +process.on("unhandledRejection", fail); |
| 148 | +process.on("uncaughtException", fail); |
| 136 | 149 | await win.waitForSelector(".monaco-workbench", { timeout: 120_000 }); |
| 137 | 150 | console.log("editor window open"); |
| 138 | 151 | await sleep(4000); |
| | @@ -149,6 +162,14 @@ |
| 149 | 162 | await palette("Git: Clone"); |
| 150 | 163 | await until("Clone from Diffinery in the picker", async () => (await quickItems()).some((t) => t.includes("Clone from Diffinery")), 30_000); |
| 151 | 164 | console.log("clone picker:", await quickItems()); |
| 165 | +if (process.env.DEBUG_ICON) |
| 166 | + console.log( |
| 167 | + await win.evaluate(async () => { |
| 168 | + const el = document.querySelector(".codicon-diffinery-logo"); |
| 169 | + const st = el && getComputedStyle(el, "::before"); |
| 170 | + return { content: st?.content, font: st?.fontFamily, fonts: [...document.fonts].map((f) => `${f.family} ${f.status}`).filter((f) => !/codicon|Segoe/.test(f)) }; |
| 171 | + }), |
| 172 | + ); |
| 152 | 173 | await shot(win, "1-clone-picker"); |
| 153 | 174 | await win.keyboard.type("Clone from Diffinery"); |
| 154 | 175 | await sleep(300); |
| | @@ -162,17 +183,14 @@ |
| 162 | 183 | const au = new URL(authorize); |
| 163 | 184 | assert.equal(au.origin + au.pathname, `${base}/oauth/authorize`); |
| 164 | 185 | assert.equal(au.searchParams.get("client_id"), `${base}/oauth/clients/editor.json`); |
| 165 | | -const scheme = au.searchParams.get("redirect_uri").split(":")[0]; |
| 186 | +const redirect = new URL(au.searchParams.get("redirect_uri")); |
| 187 | +// The extension listens on 127.0.0.1 for the browser's answer (no prompts on the way back to the editor). |
| 188 | +assert.equal(redirect.origin, `http://127.0.0.1:${redirect.port}`); |
| 189 | +assert.equal(redirect.pathname, "/callback"); |
| 166 | 190 | console.log("authorize:", authorize); |
| 167 | 191 | |
| 168 | 192 | const browser = await chromium.launch(); |
| 169 | 193 | const ctx = await browser.newContext(); |
| 170 | | -let callback; |
| 171 | | -// The consent form's answer redirects to the editor's own scheme, which the browser hands to the operating system. |
| 172 | | -ctx.on("response", (r) => { |
| 173 | | - const loc = r.headers().location; |
| 174 | | - if (loc?.startsWith(`${scheme}://`)) callback = loc; |
| 175 | | -}); |
| 176 | 194 | const page = await ctx.newPage(); |
| 177 | 195 | await page.goto(`${base}/login`); |
| 178 | 196 | await page.evaluate((v) => (document.cookie = `__Host-dfy_session=${v}; path=/; secure; samesite=lax`), ADA_SESSION); |
| | @@ -180,13 +198,14 @@ |
| 180 | 198 | const consent = ((await page.textContent("main")) ?? "").replace(/\s+/g, " "); |
| 181 | 199 | assert.match(consent, /Connect Diffinery extension for VS Code and Cursor to Diffinery/); |
| 182 | 200 | assert.match(consent, /published by localhost/); |
| 201 | +assert.match(consent, /Sends you back to\s*an app on this computer/); |
| 183 | 202 | if (SHOTS) await page.screenshot({ path: join(SHOTS, "3-consent.png"), fullPage: true }); |
| 184 | | -await page.click('button[value="approve"]', { noWaitAfter: true }).catch(() => {}); |
| 185 | | -// The browser hands the editor's own scheme to the operating system, which passes it to the editor. |
| 186 | | -const cb = await until("the redirect back to the editor", async () => callback, 30_000); |
| 187 | | -console.log("callback:", cb.replace(/code=[^&]+/, "code=…")); |
| 203 | +await page.click('button[value="approve"]'); |
| 204 | +await page.waitForURL(/^http:\/\/127\.0\.0\.1:\d+\/callback/); |
| 205 | +assert.match(await page.textContent("body"), /You're signed in to Diffinery/); |
| 206 | +if (SHOTS) await page.screenshot({ path: join(SHOTS, "3b-signed-in.png") }); |
| 207 | +console.log("browser: signed in page shown"); |
| 188 | 208 | await browser.close(); |
| 189 | | -execFileSync(CLI, [...common, "--open-url", cb], { env, stdio: "pipe" }); |
| 190 | 209 | |
| 191 | 210 | // 3. Signed in: the picker lists ada's repositories; search narrows it. |
| 192 | 211 | await until("ada's repositories in the picker", async () => (await quickItems()).some((t) => t.includes("ada/hello-editor")), 60_000); |
| | @@ -201,11 +220,11 @@ |
| 201 | 220 | await win.keyboard.press("Enter"); |
| 202 | 221 | |
| 203 | 222 | // 4. Choosing where to clone (the editor's own folder dialog), then Git clones with the extension's password. |
| 204 | | -await until("the folder dialog", async () => (await quickItems()).length > 0 || (await win.$(".quick-input-widget .quick-input-action")), 30_000).catch(() => {}); |
| 205 | | -await sleep(800); |
| 206 | | -await shot(win, "6-folder"); |
| 207 | | -await win.keyboard.press("Enter"); |
| 223 | +const destination = win.getByText("Select as Repository Destination", { exact: true }); |
| 224 | +await destination.waitFor({ timeout: 30_000 }); |
| 208 | 225 | await sleep(500); |
| 226 | +await shot(win, "6-folder"); |
| 227 | +await destination.click(); |
| 209 | 228 | const clonedReadme = join(clones, "hello-editor", "README.md"); |
| 210 | 229 | await until("the clone on disk", async () => existsSync(clonedReadme), 90_000).catch(async (e) => { |
| 211 | 230 | await shot(win, "6b-folder-stuck"); |