import test from 'node:test' import assert from 'node:assert/strict' import path from 'node:path' import { fileURLToPath } from 'node:url' import esbuild from 'esbuild' // Zwischenablage: copyTextChecked (streng, für das Pult) und copyText (verschluckt Fehler, für die Kundenansicht). // CopyableCredential.jsx wird mit einer Attrappe für react gebündelt; navigator und document sind Attrappen, die jeden Aufruf mitschreiben. // Endprüfung P3 (W2): scheitern Clipboard-API und Notweg, darf „Kopieren & öffnen“ nicht „Kopiert“ melden. async function load() { const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..') const STUB = { react: 'export const useEffect = () => {}; export const useRef = () => ({ current: null }); export const useState = (v) => [v, () => {}]', 'react/jsx-runtime': 'export const jsx = () => null; export const jsxs = () => null; export const Fragment = null', } const reactStub = { name: 'react-stub', setup(build) { build.onResolve({ filter: /^react(\/jsx-runtime)?$/ }, (a) => ({ path: a.path, namespace: 'react-stub' })) build.onLoad({ filter: /.*/, namespace: 'react-stub' }, (a) => ({ contents: STUB[a.path], loader: 'js' })) }, } const out = await esbuild.build({ stdin: { contents: "export { copyText, copyTextChecked } from './src/components/CopyableCredential.jsx'\nexport { copyAndOpen } from './src/lib/workersPult.js'", resolveDir: root, loader: 'js', }, bundle: true, format: 'esm', platform: 'neutral', write: false, jsx: 'automatic', loader: { '.jsx': 'jsx' }, plugins: [reactStub], }) return import(`data:text/javascript;base64,${Buffer.from(out.outputFiles[0].text).toString('base64')}`) } const { copyText, copyTextChecked, copyAndOpen } = await load() // Umgebung mit Protokoll. writeText: 'ok' | 'reject' | 'missing' (navigator.clipboard fehlt, z. B. unsicherer Kontext); // execCommand: true | false | 'throw' function setup({ writeText = 'ok', execCommand = true } = {}) { const log = [] const textareas = [] const clipboard = writeText === 'missing' ? undefined : { writeText: (text) => { log.push(`writeText:${text}`) return writeText === 'ok' ? Promise.resolve() : Promise.reject(new DOMException('Document is not focused.', 'NotAllowedError')) }, } const original = Object.getOwnPropertyDescriptor(globalThis, 'navigator') Object.defineProperty(globalThis, 'navigator', { value: { clipboard }, configurable: true, writable: true }) const body = { appendChild: (el) => { el.parentNode = body; log.push('append') }, removeChild: (el) => { el.parentNode = null; log.push('remove') }, } globalThis.document = { body, createElement: (tag) => { const el = { tag, value: '', style: {}, attrs: {}, parentNode: null, setAttribute(k, v) { this.attrs[k] = v }, select() { log.push('select') } } textareas.push(el) return el }, execCommand: (cmd) => { log.push(`exec:${cmd}`) if (execCommand === 'throw') throw new Error('execCommand gesperrt') return execCommand }, } return { log, textareas, restore() { if (original) Object.defineProperty(globalThis, 'navigator', original) else delete globalThis.navigator delete globalThis.document }, } } test('copyTextChecked: Clipboard-API klappt ⇒ erfüllt, kein Notweg', async () => { const env = setup({ writeText: 'ok' }) try { await copyTextChecked('Hallo') assert.deepEqual(env.log, ['writeText:Hallo']) } finally { env.restore() } }) test('copyTextChecked: Clipboard-API abgelehnt, Notweg liefert true ⇒ erfüllt; das Textfeld ist danach wieder entfernt', async () => { const env = setup({ writeText: 'reject', execCommand: true }) try { await copyTextChecked('Hallo') assert.deepEqual(env.log, ['writeText:Hallo', 'append', 'select', 'exec:copy', 'remove']) assert.equal(env.textareas.length, 1) assert.equal(env.textareas[0].value, 'Hallo') assert.equal(env.textareas[0].parentNode, null) } finally { env.restore() } }) test('copyTextChecked: kein navigator.clipboard (unsicherer Kontext) ⇒ Notweg wird benutzt', async () => { const env = setup({ writeText: 'missing', execCommand: true }) try { await copyTextChecked('Hallo') assert.deepEqual(env.log, ['append', 'select', 'exec:copy', 'remove']) } finally { env.restore() } }) test('copyTextChecked: Clipboard-API abgelehnt und execCommand liefert false ⇒ lehnt mit „Kopieren fehlgeschlagen“ ab, Textfeld entfernt', async () => { const env = setup({ writeText: 'reject', execCommand: false }) try { await assert.rejects(copyTextChecked('Hallo'), { message: 'Kopieren fehlgeschlagen' }) assert.deepEqual(env.log, ['writeText:Hallo', 'append', 'select', 'exec:copy', 'remove']) assert.equal(env.textareas[0].parentNode, null) } finally { env.restore() } }) test('copyTextChecked: wirft der Notweg selbst, wird das Textfeld trotzdem entfernt und der Fehler bleibt sichtbar', async () => { const env = setup({ writeText: 'reject', execCommand: 'throw' }) try { await assert.rejects(copyTextChecked('Hallo'), { message: 'execCommand gesperrt' }) assert.equal(env.log.at(-1), 'remove') assert.equal(env.textareas[0].parentNode, null) } finally { env.restore() } }) test('copyText (Kundenansicht): verschluckt jeden Fehlschlag wie bisher und wirft nie', async () => { for (const options of [{ writeText: 'reject', execCommand: false }, { writeText: 'reject', execCommand: 'throw' }, { writeText: 'missing', execCommand: false }]) { const env = setup(options) try { assert.equal(await copyText('Hallo'), undefined) } finally { env.restore() } } const env = setup({ writeText: 'ok' }) try { await copyText('Hallo') assert.deepEqual(env.log, ['writeText:Hallo']) } finally { env.restore() } }) test('Pult „Kopieren & öffnen“: scheitern beide Kopierwege, meldet copyAndOpen copied:false (die Quelle wird trotzdem geöffnet)', async () => { const env = setup({ writeText: 'reject', execCommand: false }) try { const opened = [] const result = await copyAndOpen({ text: 'Antwort', url: 'https://www.kleinanzeigen.de/s-anzeige/x/1', copy: copyTextChecked, open: (u) => { opened.push(u); env.log.push('open') } }) assert.deepEqual(result, { copied: false, opened: true }) assert.deepEqual(opened, ['https://www.kleinanzeigen.de/s-anzeige/x/1']) // writeText steht vor dem Öffnen (Nutzer-Geste), die Ablehnung wird danach abgewartet assert.ok(env.log.indexOf('writeText:Antwort') < env.log.indexOf('open')) } finally { env.restore() } }) test('Pult „Kopieren & öffnen“: Notweg klappt ⇒ copied:true; Clipboard-API klappt ⇒ copied:true, writeText vor open', async () => { let env = setup({ writeText: 'reject', execCommand: true }) try { assert.deepEqual(await copyAndOpen({ text: 'A', url: 'https://example.org/x', copy: copyTextChecked, open: () => {} }), { copied: true, opened: true }) } finally { env.restore() } env = setup({ writeText: 'ok' }) try { const result = await copyAndOpen({ text: 'B', url: 'https://example.org/x', copy: copyTextChecked, open: () => env.log.push('open') }) assert.deepEqual(result, { copied: true, opened: true }) assert.deepEqual(env.log, ['writeText:B', 'open']) } finally { env.restore() } }) test('Pult „Kopieren & öffnen“ mit der verschluckenden Fassung würde fälschlich copied:true melden (darum nimmt PultList copyTextChecked)', async () => { const env = setup({ writeText: 'reject', execCommand: false }) try { assert.deepEqual(await copyAndOpen({ text: 'C', url: 'https://example.org/x', copy: copyText, open: () => {} }), { copied: true, opened: true }) } finally { env.restore() } })