/** * Die Anzeigegröße im echten Fenster – und der Umbruch, den sie erzwingt. * * Der Zoom hängt an den `webContents`, nicht am Dokument – prüfbar ist er * deshalb nur am laufenden Fenster. Die reine Rechnerei (welche Stufe folgt * auf welche, wie werden unbrauchbare Werte bereinigt) prüft * `tests/ansicht.test.ts`. * * Der zweite Teil dieser Datei misst WCAG 2.2 – 1.4.10 (Reflow) an dem Punkt, * an dem das Kriterium ihn festmacht: 400 Prozent Vergrößerung auf einem * 1280 Pixel breiten Fenster, also 320 CSS-Pixel Inhaltsbreite. */ import { type ElectronApplication, type Page } from '@playwright/test'; import { amStart, appStarten, bauPruefen } from './electron-hilfe'; import { expect, test } from './konsolenwache'; let app: ElectronApplication | undefined; let fenster: Page; test.beforeAll(async () => { // Fehlender oder veralteter Bau ist ein Fehler, kein Grund zum Schweigen. bauPruefen(); ({ app, fenster } = await appStarten()); }); test.afterAll(async () => { await app?.close(); }); async function faktor(): Promise { return ( (await app?.evaluate(({ BrowserWindow }) => Number(BrowserWindow.getAllWindows()[0]?.webContents.getZoomFactor().toFixed(3)), )) ?? 0 ); } test.describe('Anzeigegröße', () => { test('beginnt bei 100 Prozent', async () => { await expect(fenster.getByText('Aktuell: 100 Prozent')).toBeVisible(); expect(await faktor()).toBeCloseTo(1, 2); }); test('vergrößert die ganze Anwendung, nicht nur den Text', async () => { /* Der Zoomfaktor wirkt auf alles im Fenster – auch auf die siebzehn Prüfzeichen, bei denen das Bild die Antwort ist. Genau deshalb Zoom und nicht bloß eine größere Schrift. */ await fenster.getByRole('button', { name: /Größer/u }).click(); await expect(fenster.getByText('Aktuell: 110 Prozent')).toBeVisible(); expect(await faktor()).toBeCloseTo(1.1, 2); }); test('reicht bis 400 Prozent (WCAG 1.4.4 und 1.4.10)', async () => { /* 1.4.4 verlangt 200 Prozent. Die Leiter geht weiter bis 400, weil 1.4.10 dort misst: 400 Prozent auf 1280 Pixeln Fensterbreite sind die 320 CSS-Pixel, bei denen der Inhalt ohne seitliches Rollen benutzbar sein muss. Endete die Leiter bei 200, wäre dieser Fall mit Bordmitteln nicht einmal herstellbar. Nach jedem Klick auf die neue Zahl warten. Blind hintereinander zu klicken läuft in eine Wettlaufsituation: Playwright prüft, ob der Knopf bedienbar ist, React schaltet ihn zwischendurch ab. */ const knopf = fenster.getByRole('button', { name: /Größer/u }); const stand = fenster.locator('.anzeigegroesse__stand'); for (const stufe of [125, 150, 175, 200, 250, 300, 400]) { await knopf.click(); await expect(stand).toHaveText(`Aktuell: ${String(stufe)} Prozent`); } await expect(fenster.getByText('Aktuell: 400 Prozent')).toBeVisible(); expect(await faktor()).toBeCloseTo(4, 2); await expect(fenster.getByRole('button', { name: /Größer/u })).toBeDisabled(); }); test('bleibt bei 400 Prozent bedienbar', async () => { /* Bei vierfacher Größe muss sich alles noch erreichen lassen – sonst wäre die Vergrößerung wertlos. Geprüft wird an der Hauptschaltfläche. */ const weiterlernen = fenster.getByRole('button', { name: /Weiterlernen/u }); await expect(weiterlernen).toBeVisible(); await weiterlernen.scrollIntoViewIfNeeded(); await expect(weiterlernen).toBeEnabled(); }); test('kehrt auf Normalgröße zurück', async () => { await fenster.getByRole('button', { name: 'Normalgröße' }).click(); await expect(fenster.getByText('Aktuell: 100 Prozent')).toBeVisible(); expect(await faktor()).toBeCloseTo(1, 2); }); test('überlebt einen Neustart der Anwendung', async () => { await fenster.getByRole('button', { name: /Größer/u }).click(); await fenster.getByRole('button', { name: /Größer/u }).click(); await expect(fenster.getByText('Aktuell: 125 Prozent')).toBeVisible(); /* Neu laden ersetzt keinen echten Neustart, prüft aber denselben Weg: Der Hauptprozess legt die gespeicherte Größe auf das frisch geladene Dokument. Ohne das blitzte die Anwendung beim Start klein auf. */ await fenster.reload(); await fenster.getByRole('heading', { level: 1 }).waitFor(); expect(await faktor()).toBeCloseTo(1.25, 2); await expect(fenster.getByText('Aktuell: 125 Prozent')).toBeVisible(); await fenster.getByRole('button', { name: 'Normalgröße' }).click(); }); /* Ganz am Ende, und das ist Absicht: Die Tests darueber haengen aneinander - „kehrt auf Normalgroesse zurueck" verlaesst sich darauf, dass der vorige Test bei der obersten Stufe endet, sonst ist der Knopf gesperrt. Dazwischen eingeschoben brach dieser Block die Kette. Er stellt am Anfang jedes Tests selbst her, was er braucht. */ test.describe('über die Tastatur', () => { /* Gemeldet und nachgemessen: `Strg` und `+` taten unter Windows nichts. Electron liest den Beschleunigernamen `Plus` als die umgeschaltete Form von `=` und meldet damit faktisch `Strg+Umschalt+=` beim Betriebssystem an – richtig für eine amerikanische Tastatur, falsch für eine deutsche, auf der `+` eine eigene Taste ohne Umschalt ist. Abgefangen wird deshalb im Renderer (`hooks/useZoomtasten.ts`), und zwar über das Zeichen, das die Tastatur tatsächlich liefert. Damit ist es auch prüfbar: Ein Menü-Beschleuniger läuft über die Beschleunigertabelle des Betriebssystems und wird von keinem Prüflauf erreicht – nachgemessen ebenso wie `before-input-event` im Hauptprozess, das für Tasten aus einem Prüflauf überhaupt nichts meldet. */ test.beforeEach(async () => { await fenster.keyboard.press('Control+0'); await fenster.waitForTimeout(200); }); test('vergrößert über alle drei Schreibweisen des Pluszeichens', async () => { // Die deutsche Plustaste: dieselbe Taste wie `=` auf US-Layout. await fenster.keyboard.press('Control+='); await fenster.waitForTimeout(200); expect(await faktor(), 'Strg und + (deutsche Tastatur)').toBeCloseTo(1.1, 2); // Die amerikanische Schreibweise, mit Umschalt. await fenster.keyboard.press('Control+Shift+='); await fenster.waitForTimeout(200); expect(await faktor(), 'Strg, Umschalt und +').toBeCloseTo(1.25, 2); // Der Ziffernblock. await fenster.keyboard.press('Control+NumpadAdd'); await fenster.waitForTimeout(200); expect(await faktor(), 'Strg und + auf dem Ziffernblock').toBeCloseTo(1.5, 2); }); test('verkleinert und setzt zurück', async () => { await fenster.keyboard.press('Control+='); await fenster.keyboard.press('Control+='); await fenster.waitForTimeout(250); expect(await faktor()).toBeCloseTo(1.25, 2); await fenster.keyboard.press('Control+-'); await fenster.waitForTimeout(200); expect(await faktor()).toBeCloseTo(1.1, 2); await fenster.keyboard.press('Control+0'); await fenster.waitForTimeout(200); expect(await faktor()).toBeCloseTo(1, 2); }); test('wirkt auch, wenn der Fokus in einem Textfeld steht', async () => { /* Wer eine offene Frage tippt und die Schrift zu klein findet, soll nicht erst irgendwohin klicken müssen. */ const feld = fenster.getByLabel('Ihr Prüfungstermin'); await feld.click(); await fenster.keyboard.press('Control+='); await fenster.waitForTimeout(200); expect(await faktor()).toBeCloseTo(1.1, 2); await fenster.keyboard.press('Control+0'); await fenster.waitForTimeout(200); }); test('schluckt keine fremde Tastenkombination', async () => { /* Der Zuhörer liegt am Fenster und sieht jede Taste mit Strg. Er darf nur die drei eigenen abfangen – sonst bräche er Kopieren, Einfügen und alles Weitere. */ const vorher = await faktor(); for (const taste of ['Control+a', 'Control+c', 'Control+v', 'Control+1', 'Control+9']) { await fenster.keyboard.press(taste); } await fenster.waitForTimeout(250); expect(await faktor()).toBeCloseTo(vorher, 2); }); }); }); /* ===================================================================== Reflow – WCAG 2.2, 1.4.10 ===================================================================== */ /** Was eine Messung zurückgibt. */ interface Reflowmass { /** Sichtbare Breite des Dokuments in CSS-Pixeln, ohne Rollbalken. */ readonly innen: number; /** Tatsächliche Breite des Dokuments; darf `innen` nicht überschreiten. */ readonly doku: number; /** Elemente, deren Inhalt über den eigenen Kasten hinausragt. */ readonly ueberlauf: readonly string[]; } /** * Misst den waagerechten und den senkrechten Überlauf der stehenden Ansicht. * * ## Was gegenüber der ersten Fassung schärfer geworden ist * * Die Vorgängerin dieser Messung war zu großzügig, und zwar an vier Stellen * zugleich. Nachgezählt: * * 1. **Der Filter warf zu viel weg.** `kasten.width <= 1 || kasten.height <= 1` * verwarf jedes Element, das in *einer* Richtung dünn ist – eine Trennlinie, * ein Fortschrittsbalken, ein Rahmenstreifen. Gemeint war der Klammergriff * für Screenreader-Texte (`.nur-screenreader`), und der ist in **beiden** * Richtungen 1 Pixel groß. Jetzt steht dort `&&`. * 2. **Die Toleranz war fast 2 Pixel.** Verglichen wurde `scrollWidth` (ohne * Rahmen) gegen `Math.ceil(kasten.width) + 1` (mit Rahmen). Bei den hier * üblichen 2 Pixel starken Rahmen ließ das rund 4 Pixel Überstand * durchgehen. Jetzt stehen `scrollWidth` und `clientWidth` gegeneinander – * beide ohne Rahmen – mit einer Toleranz von 1 Pixel für die Rundung: Beide * Werte sind ganzzahlig, ein Layout mit Bruchteilen kann sie um eins * auseinanderfallen lassen. * 3. **Der Bezug war zu weit.** `window.innerWidth` zählt den senkrechten * Rollbalken mit; die Seite durfte also um dessen Breite zu breit sein. * Gemessen wird jetzt gegen `documentElement.clientWidth`. * 4. **Senkrecht wurde gar nicht gemessen.** Abgeschnittener Inhalt in einem * Kasten mit fester Höhe fiel nicht auf. Jetzt fällt er auf – ausgenommen * die Seite selbst, denn senkrechtes Rollen erlaubt 1.4.10 ausdrücklich. * * Ausgenommen bleiben drei Fälle, jeder mit Grund: * * - **Absichtliche Rollbereiche** (`overflow-x`/`-y: auto` oder `scroll`). * Genau dort liegt die Verlaufstabelle, und für Inhalte, die eine * zweidimensionale Anordnung brauchen, lässt 1.4.10 das zu. * - **Textfelder und Auswahllisten.** Ein `` rollt seinen eigenen Wert * von Haus aus; das ist keine verlorene Information. * - **Unsichtbares** – Kästen ohne Fläche und `visibility: hidden`. */ async function reflowMessen(seite: Page): Promise { return seite.evaluate(() => { const wurzel = document.documentElement; const ueberlauf: string[] = []; for (const element of [...document.querySelectorAll('*')]) { const kasten = element.getBoundingClientRect(); if (kasten.width === 0 || kasten.height === 0) { continue; } if (kasten.width <= 1 && kasten.height <= 1) { continue; } if (element.tagName === 'INPUT' || element.tagName === 'SELECT') { continue; } const stil = getComputedStyle(element); if (stil.visibility === 'hidden') { continue; } const istSeite = element === wurzel || element === document.body; const rolltWaagerecht = stil.overflowX === 'auto' || stil.overflowX === 'scroll'; const rolltSenkrecht = istSeite || stil.overflowY === 'auto' || stil.overflowY === 'scroll'; const klasse = element.getAttribute('class'); const name = element.tagName + (klasse === null ? '' : `.${klasse.slice(0, 40)}`); const zuBreit = element.scrollWidth - element.clientWidth; if (!rolltWaagerecht && zuBreit > 1) { ueberlauf.push(`${name}: ${String(zuBreit)} px zu breit`); } const zuHoch = element.scrollHeight - element.clientHeight; if (!rolltSenkrecht && zuHoch > 1) { ueberlauf.push(`${name}: ${String(zuHoch)} px zu hoch`); } } return { innen: wurzel.clientWidth, doku: wurzel.scrollWidth, // Zwölf reichen zum Suchen; hundert Zeilen im Fehlerbericht helfen nicht. ueberlauf: ueberlauf.slice(0, 12), }; }); } /** Setzt die Anzeigegröße über denselben Weg, den auch die Oberfläche geht. */ async function anzeigegroesseSetzen(prozent: number): Promise { await fenster.evaluate(async (wert: number) => { const bruecke = ( window as unknown as { lernApp?: { anzeigegroesseSetzen: (p: number) => Promise }; } ).lernApp; await bruecke?.anzeigegroesseSetzen(wert); }, prozent); await fenster.waitForTimeout(250); } test.describe('Reflow bei 320 CSS-Pixeln (WCAG 1.4.10)', () => { /* Eigener Block am Ende, aus demselben Grund wie der Tastaturblock: Er verstellt Fenstergröße und Anzeigegröße und stellt beides danach wieder her. Dazwischen eingeschoben zerrisse er die Kette der Tests darüber. 1280 Pixel Inhaltsbreite mal 400 Prozent ergibt genau die 320 CSS-Pixel, an denen das Kriterium gemessen wird. `setContentSize` und nicht `setSize`: Der Fensterrahmen zählt nicht zum Inhalt, und um den geht es. */ const URSPRUNG = { breite: 1180, hoehe: 820 }; test.beforeAll(async () => { await app?.evaluate(({ BrowserWindow }) => { BrowserWindow.getAllWindows()[0]?.setContentSize(1280, 1024); }); await fenster.waitForTimeout(300); }); test.afterAll(async () => { await anzeigegroesseSetzen(100); await app?.evaluate(({ BrowserWindow }, groesse) => { BrowserWindow.getAllWindows()[0]?.setContentSize(groesse.breite, groesse.hoehe); }, URSPRUNG); }); /** Zurück auf den Startbildschirm, egal wo der vorige Test aufgehört hat. */ async function zumStart(): Promise { if ((await fenster.getByRole('dialog').count()) > 0) { await fenster.keyboard.press('Escape'); await fenster.waitForTimeout(150); } const ausstiege = [ /^Ohne Auswertung verwerfen$/u, /^Auswertung anzeigen$/u, /^Simulation abbrechen$/u, /^Sitzung beenden$/u, /^Zum Start$/u, ]; for (let i = 0; i < 8; i++) { if (await amStart(fenster)) { return; } let geklickt = false; for (const weg of ausstiege) { const knopf = fenster.getByRole('button', { name: weg }); if ((await knopf.count()) > 0 && (await knopf.first().isVisible())) { await knopf.first().click(); await fenster.waitForTimeout(250); geklickt = true; break; } } if (!geklickt) { return; } } } /** * Die Ansichten, die der Prüfplan als Härtefälle führt (Zeile H8). * * Hingeführt wird bei 100 Prozent: Auf 320 CSS-Pixeln liegt zwischen zwei * Schaltflächen ein halber Bildschirm, und jeder Klick würde zur * Rollübung. Gemessen wird danach – der Zustand ist derselbe. */ const ANSICHTEN: readonly { readonly name: string; readonly hin: () => Promise }[] = [ { name: 'Startbildschirm mit Lernstand', hin: async () => { await zumStart(); }, }, { name: 'Hilfe', hin: async () => { await zumStart(); await fenster.getByRole('contentinfo').getByRole('button', { name: 'Hilfe' }).click(); await fenster.getByRole('dialog', { name: 'Hilfe' }).waitFor(); }, }, { name: 'Trefferliste der Suche', hin: async () => { /* Der Suchbegriff ist mit Bedacht gewählt: „waffenbesitzkarten- pflichtigen" ist mit 29 Zeichen das längste Wort im amtlichen Katalog. Damit steht der harte Fall des Prüfplans – ein langer amtlicher Text – zuverlässig in der Liste und nicht nur zufällig. */ await zumStart(); await fenster.getByRole('button', { name: /Fragen durchsuchen/u }).click(); await fenster.getByRole('heading', { name: 'Fragen durchsuchen', level: 1 }).waitFor(); await fenster.getByLabel('Suchbegriff').fill('waffenbesitzkartenpflichtigen'); await fenster.waitForTimeout(500); }, }, { name: 'Lernsitzung nach der Antwort', hin: async () => { await zumStart(); await fenster.getByRole('button', { name: /Weiterlernen/u }).click(); await fenster .getByRole('button', { name: /^(Antwort bestätigen|Musterantwort anzeigen)$/u }) .waitFor(); const auswahlfeld = fenster.getByRole('radio').or(fenster.getByRole('checkbox')); if ((await auswahlfeld.count()) > 0) { await auswahlfeld.first().click(); await fenster.getByRole('button', { name: 'Antwort bestätigen' }).click(); } else { await fenster.getByRole('button', { name: 'Musterantwort anzeigen' }).click(); await fenster.getByRole('button', { name: 'Nicht gewusst' }).click(); } await fenster.locator('.ergebnis').first().waitFor(); }, }, { name: 'Prüfungslauf mit Fragenübersicht', hin: async () => { await zumStart(); await fenster.getByRole('button', { name: /Prüfungssimulation vorbereiten/u }).click(); await fenster.getByRole('radio', { name: /Selbst einstellen/u }).check(); await fenster.getByLabel('Fragen im Bogen', { exact: true }).fill('40'); await fenster.getByLabel('Bearbeitungszeit', { exact: true }).fill('60'); await fenster.getByLabel('Anteil offener Fragen', { exact: true }).fill('0'); await fenster.getByRole('button', { name: 'Simulation starten' }).click(); await fenster.getByRole('button', { name: 'Simulation abgeben' }).waitFor(); /* Eine Antwort setzen: Erst dann führt die Übersicht beide Zustände nebeneinander, und erst dann ist ihr Raster wirklich gefüllt. */ await fenster.getByRole('radio').or(fenster.getByRole('checkbox')).first().click(); }, }, { name: 'Prüfungsauswertung mit Verlaufstabelle', hin: async () => { /* Kein `zumStart()`: Dieser Schritt gibt den Bogen ab, den der vorige gestartet hat. Ihn eigens noch einmal zusammenzustellen kostete einen zweiten Durchlauf durch die Prüfungswahl, und die Auswertung bekäme dieselben Zahlen. */ await fenster.getByRole('button', { name: 'Simulation abgeben' }).click(); await fenster.getByRole('button', { name: 'Jetzt abgeben und auswerten' }).click(); await fenster .getByRole('heading', { name: 'Auswertung der Prüfungssimulation', level: 1 }) .waitFor(); /* Der Verlauf kommt aus der Datenbank nach. Ohne dieses Warten misst der Lauf den Ladehinweis statt der Tabelle. */ await fenster.getByText('Der Verlauf wird geladen …').waitFor({ state: 'hidden' }); }, }, ]; for (const ansicht of ANSICHTEN) { test(ansicht.name, async () => { await anzeigegroesseSetzen(100); await ansicht.hin(); await anzeigegroesseSetzen(400); const mass = await reflowMessen(fenster); expect( mass.innen, `Gemessen wird bei ${String(mass.innen)} CSS-Pixeln – das ist nicht der Prüffall.`, ).toBeLessThanOrEqual(320); expect( mass.innen, `Nur ${String(mass.innen)} CSS-Pixel: Die Messung wäre strenger als das Kriterium.`, ).toBeGreaterThan(300); expect( mass.doku, `${ansicht.name}: Die Seite ist ${String(mass.doku)} px breit, sichtbar sind ` + `${String(mass.innen)} px – seitliches Rollen für Fließtext.`, ).toBeLessThanOrEqual(mass.innen); expect(mass.ueberlauf, `${ansicht.name}: Inhalt ragt über seinen Kasten hinaus.`).toEqual([]); }); } test('die Messung fängt einen absichtlich erzeugten Überlauf', async () => { /* Gegenprobe zur Gegenprobe. Eine Messung, die immer grün ist, sagt nichts. Hier bekommt die Seite nacheinander drei Fehler eingesetzt, die sie in Wirklichkeit nicht hat – einen Kasten zwei Pixel breiter als sein Elternteil, ein unteilbares langes Wort und einen abgeschnittenen Kasten. Jeder muss auffallen, danach muss die Seite wieder sauber sein. Die zwei Pixel sind der Punkt: Die frühere Messung ließ durch ihren Bezug auf `getBoundingClientRect().width` plus Rundung rund vier Pixel durchgehen und hätte diesen Fall verschwiegen. */ await anzeigegroesseSetzen(100); await zumStart(); await anzeigegroesseSetzen(400); const vorher = await reflowMessen(fenster); expect(vorher.ueberlauf, 'Die Ansicht ist vor der Gegenprobe nicht sauber.').toEqual([]); for (const fall of ['zweiPixel', 'langesWort', 'abgeschnitten'] as const) { await fenster.evaluate((art: string) => { const wirt = document.querySelector('.karte'); if (wirt === null) { throw new Error('Keine Karte gefunden, an der sich messen ließe.'); } const stoerer = document.createElement('div'); stoerer.id = 'gegenprobe'; if (art === 'zweiPixel') { stoerer.style.inlineSize = `${String(wirt.clientWidth + 2)}px`; stoerer.style.blockSize = '20px'; } else if (art === 'langesWort') { stoerer.style.whiteSpace = 'nowrap'; stoerer.textContent = 'Deaktivierungsdurchfuehrungsverordnungsaenderungsgesetz'; } else { stoerer.style.blockSize = '20px'; stoerer.style.overflow = 'hidden'; stoerer.textContent = 'Eine Zeile, zwei Zeilen, drei Zeilen, vier Zeilen, fuenf Zeilen.'; } wirt.appendChild(stoerer); }, fall); const mitFehler = await reflowMessen(fenster); expect( mitFehler.ueberlauf.length, `Die Messung übersieht den Fall „${fall}".`, ).toBeGreaterThan(0); await fenster.evaluate(() => { document.getElementById('gegenprobe')?.remove(); }); } const nachher = await reflowMessen(fenster); expect(nachher.ueberlauf, 'Die Gegenprobe hat etwas hinterlassen.').toEqual([]); expect(nachher.doku).toBeLessThanOrEqual(nachher.innen); }); test('das Fenster lässt sich bis auf 320 CSS-Pixel schmal ziehen', async () => { /* Der zweite Weg zu 320 Pixeln, unabhängig vom Zoom: das Fenster selbst. Bis Fassung 0.20.0 stand `minWidth` auf 720 – zusammen mit einer Leiter bis 200 Prozent war der schmalste erreichbare Bereich 360 CSS-Pixel, der Prüffall also nicht herstellbar. Geprüft wird an der eingestellten Mindestgröße und nicht an der gemessenen Breite: `minWidth` meint die Fenster-, nicht die Inhaltsbreite, und wie viel der Rahmen davon abzieht, entscheidet das Betriebssystem. */ const minBreite = (await app?.evaluate( ({ BrowserWindow }) => BrowserWindow.getAllWindows()[0]?.getMinimumSize()[0] ?? 0, )) ?? 0; expect(minBreite, 'Das Fenster lässt sich nicht schmal genug ziehen.').toBeLessThanOrEqual(400); expect( minBreite, 'Unter 320 Pixeln gibt es keine Zusage mehr – so schmal soll es nicht gehen.', ).toBeGreaterThanOrEqual(320); // Und die Ansicht trägt auch bei dieser Breite, bei 100 Prozent Anzeige. await anzeigegroesseSetzen(100); await zumStart(); await app?.evaluate(({ BrowserWindow }) => { BrowserWindow.getAllWindows()[0]?.setSize(200, 800); }); await fenster.waitForTimeout(300); const mass = await reflowMessen(fenster); expect(mass.doku, 'Am schmalsten Fenster rollt die Seite seitlich.').toBeLessThanOrEqual( mass.innen, ); expect(mass.ueberlauf, 'Am schmalsten Fenster ragt Inhalt aus seinem Kasten.').toEqual([]); await app?.evaluate(({ BrowserWindow }) => { BrowserWindow.getAllWindows()[0]?.setContentSize(1280, 1024); }); await fenster.waitForTimeout(300); }); });