Spaces:
Sleeping
Sleeping
| // Public share links, driven through a real browser: the owner creates one in | |
| // the Share dialog, and a visitor with NO account opens it. | |
| // node test/share-link.js | |
| // Its own file rather than a block inside browser.js: it has to clear the | |
| // session cookie to become a stranger, and that is a rude thing to do in the | |
| // middle of a suite that stays signed in throughout. | |
| import { spawn } from 'node:child_process' | |
| import fs from 'node:fs' | |
| import path from 'node:path' | |
| import assert from 'node:assert' | |
| import { fileURLToPath } from 'node:url' | |
| import { chromium } from 'playwright-core' | |
| const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..') | |
| const PORT = 3311 | |
| const BASE = `http://localhost:${PORT}` | |
| const DATA = path.join(root, '.share-link-data') | |
| const SHOT = process.env.SHOT_DIR || '/tmp/claude-1000/ui-shots' | |
| fs.rmSync(DATA, { recursive: true, force: true }) | |
| fs.mkdirSync(SHOT, { recursive: true }) | |
| const server = spawn('node', ['server/index.js'], { | |
| cwd: root, | |
| // HOST is what the server puts in the prompt's curl examples; the link the | |
| // dialog shows is built from the origin the browser is actually on | |
| env: { ...process.env, PORT: String(PORT), DATA_DIR: DATA, OAUTH_CLIENT_ID: '', OAUTH_CLIENT_SECRET: '', HOST: BASE }, | |
| stdio: ['ignore', 'pipe', 'pipe'], | |
| }) | |
| server.stderr.on('data', d => process.stderr.write('[server] ' + d)) | |
| const wait = ms => new Promise(r => setTimeout(r, ms)) | |
| async function waitFor(fn, what, ms = 15000) { | |
| const t0 = Date.now() | |
| while (Date.now() - t0 < ms) { | |
| try { | |
| if (await fn()) return | |
| } catch {} | |
| await wait(150) | |
| } | |
| throw new Error('timeout: ' + what) | |
| } | |
| async function main() { | |
| await waitFor(async () => (await fetch(`${BASE}/healthz`)).ok, 'server') | |
| const browser = await chromium.launch({ | |
| headless: true, | |
| executablePath: process.env.PW_EXECUTABLE || undefined, | |
| args: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage', '--single-process'], | |
| }) | |
| const context = await browser.newContext({ viewport: { width: 1440, height: 900 } }) | |
| const page = await context.newPage() | |
| const consoleErrors = [] | |
| page.on('pageerror', e => consoleErrors.push('PAGEERROR: ' + e.message)) | |
| page.on('console', m => { | |
| if (m.type() === 'error' && !m.text().includes('fonts.g')) consoleErrors.push(m.text()) | |
| }) | |
| // --- the owner writes something and publishes a link --- | |
| await page.goto(`${BASE}/auth/dev?u=alice&next=/`) | |
| const doc = await (await page.request.post(`${BASE}/api/docs`, { data: { title: 'Link Shared' } })).json() | |
| await page.goto(`${BASE}/d/${doc.id}`, { waitUntil: 'networkidle' }) | |
| await page.waitForSelector('.tiptap') | |
| await page.click('.tiptap h1') | |
| await page.keyboard.press('End') | |
| await page.keyboard.press('Enter') | |
| await page.keyboard.type('Readable by anyone holding the link.') | |
| await wait(1200) | |
| await page.click('#share-btn') | |
| await page.waitForSelector('#share-pop:not(.hidden)') | |
| await page.waitForSelector('#link-share:not(.hidden)') | |
| await page.click('#link-toggle') | |
| await page.waitForSelector('#link-row:not(.hidden)') | |
| await waitFor(async () => /\/p\//.test(await page.inputValue('#link-url')), 'the link appears in the dialog') | |
| const link = await page.inputValue('#link-url') | |
| assert.ok(link.startsWith(BASE), 'the link uses the origin the browser is on: ' + link) | |
| assert.ok(/\/p\/[A-Za-z0-9_-]{32,}$/.test(link), 'long unguessable path: ' + link) | |
| await page.screenshot({ path: path.join(SHOT, 'share-dialog.png') }) | |
| console.log('✓ owner creates a link from the Share dialog') | |
| // --- a stranger: no session cookie, no account --- | |
| await context.clearCookies() | |
| await page.goto(link, { waitUntil: 'networkidle' }) | |
| await page.waitForSelector('.tiptap') | |
| await waitFor(async () => (await page.textContent('.tiptap')).includes('Readable by anyone holding the link'), 'a visitor reads the document') | |
| const visitor = await page.evaluate(() => ({ | |
| editable: window.__editor?.isEditable, | |
| signin: !document.getElementById('signin')?.classList.contains('hidden'), | |
| share: getComputedStyle(document.getElementById('share-btn')).display, | |
| agents: getComputedStyle(document.getElementById('agents-btn')).display, | |
| presence: getComputedStyle(document.getElementById('presence')).display, | |
| whoami: document.getElementById('whoami')?.textContent || '', | |
| })) | |
| assert.equal(visitor.editable, false, 'the document is not editable for a visitor') | |
| assert.equal(visitor.signin, false, 'no sign-in wall for a link visitor') | |
| assert.equal(visitor.share, 'none', 'no Share button') | |
| assert.equal(visitor.agents, 'none', 'no Agents button') | |
| assert.equal(visitor.presence, 'none', 'the presence row is not rendered for a stranger (hidden, not withheld — see app.css)') | |
| assert.match(visitor.whoami, /Read-only/, 'the header says what this is: ' + visitor.whoami) | |
| await page.screenshot({ path: path.join(SHOT, 'share-visitor.png') }) | |
| console.log('✓ visitor reads it: no account, no sign-in wall, no write UI') | |
| // typing must change nothing — the socket refuses writes from a link | |
| await page.click('.tiptap p').catch(() => {}) | |
| await page.keyboard.type('VANDALISM') | |
| await wait(1500) | |
| await page.goto(`${BASE}/auth/dev?u=alice&next=/d/${doc.id}`, { waitUntil: 'networkidle' }) | |
| const after = await (await page.request.get(`${BASE}/api/docs/${doc.id}`)).json() | |
| assert.ok(!after.markdown.includes('VANDALISM'), 'a visitor cannot write: ' + after.markdown.slice(0, 90)) | |
| console.log('✓ a visitor typing into it changes nothing') | |
| // checked here: the revoke step below visits a dead link on purpose, and a | |
| // 404 in the console is the correct outcome of that, not a defect | |
| assert.deepEqual(consoleErrors, [], 'no console errors: ' + JSON.stringify(consoleErrors)) | |
| console.log('✓ zero console errors') | |
| // --- turning it off closes the door on the link already handed out --- | |
| await page.waitForSelector('.tiptap') | |
| await page.click('#share-btn') | |
| await page.waitForSelector('#link-share:not(.hidden)') | |
| await page.click('#link-toggle') | |
| await waitFor(async () => (await page.textContent('#link-hint')).startsWith('Off'), 'dialog shows the link is off') | |
| await context.clearCookies() | |
| const revoked = await page.goto(link, { waitUntil: 'domcontentloaded' }) | |
| assert.equal(revoked.status(), 404, 'a revoked link is a 404') | |
| const stillDead = await page.evaluate(() => !document.getElementById('signin')?.classList.contains('hidden')) | |
| assert.ok(stillDead, 'and it shows the sign-in shell, not the document') | |
| console.log('✓ revoking kills a link that was already handed out') | |
| await browser.close() | |
| server.kill() | |
| fs.rmSync(DATA, { recursive: true, force: true }) | |
| console.log('\nSHARE LINK TEST PASSED') | |
| process.exit(0) | |
| } | |
| main().catch(async err => { | |
| console.error('\nSHARE LINK TEST FAILED:', err.message) | |
| server.kill() | |
| process.exit(1) | |
| }) | |