From 968fd740fcd8c2964edaf7505f1fe19a2875c0f2 Mon Sep 17 00:00:00 2001 From: gusthoff Date: Fri, 24 Jul 2026 20:50:25 +0200 Subject: [PATCH 1/7] Frontend: stop eslint from linting generated Sphinx build output eslint.config.js registered eslint-plugin-jsdoc's flat/recommended config with no `files` restriction, unlike the sibling TypeScript config block that is scoped to src/**/*.ts and tests/**/*.ts. Because ESLint's flat config does not consult .gitignore, this meant that any file ESLint happened to find under tests/ was linted with the jsdoc rules, including frontend/tests/html/ -- a Sphinx documentation build output directory (gitignored, regenerated by the test suite) that contains furo theme JavaScript (search stemmers, sphinx_highlight.js, etc.) copied there by Sphinx itself, not written by this project. This accounted for 190 of 205 eslint warnings (93%) whenever that directory happened to exist on disk from a prior local build/test run. Fixed by adding a global `ignores: ['tests/html/**']` entry and scoping the jsdoc config block to the same files glob as the TypeScript block. Co-Authored-By: Claude Sonnet 5 --- frontend/eslint.config.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index d75275572..8a9657c2f 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -3,7 +3,14 @@ import tsParser from '@typescript-eslint/parser'; import jsdocPlugin from 'eslint-plugin-jsdoc'; export default [ - jsdocPlugin.configs['flat/recommended'], + { + // tests/html/ is regenerated Sphinx build output (gitignored), not project code. + ignores: ['tests/html/**'], + }, + { + ...jsdocPlugin.configs['flat/recommended'], + files: ['src/**/*.ts', 'tests/**/*.ts'], + }, { files: ['src/**/*.ts', 'tests/**/*.ts'], plugins: { From 96fc13dd72011ae9967b2f55475681c108ab8b2a Mon Sep 17 00:00:00 2001 From: gusthoff Date: Fri, 24 Jul 2026 20:51:03 +0200 Subject: [PATCH 2/7] Frontend: recognize chai assertions in the no-unused-expressions rule @typescript-eslint/no-unused-expressions doesn't know that a chai assertion chain like expect(x).to.be.true is a meaningful statement -- it just sees a property-access expression whose result is discarded, and flags it as dead code. This produced 26 false-positive errors across the test spec files, none of which were real bugs. Added eslint-plugin-chai-friendly and swapped in its chai-aware no-unused-expressions rule, scoped to tests/**/*.ts, in place of the plain rule. src/**/*.ts keeps the plain rule since it has no chai assertions to misread. Co-Authored-By: Claude Sonnet 5 --- frontend/eslint.config.js | 12 ++++++++++++ frontend/package.json | 1 + frontend/pnpm-lock.yaml | 13 +++++++++++++ 3 files changed, 26 insertions(+) diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index 8a9657c2f..6ec0b1ad3 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -1,6 +1,7 @@ import tsPlugin from '@typescript-eslint/eslint-plugin'; import tsParser from '@typescript-eslint/parser'; import jsdocPlugin from 'eslint-plugin-jsdoc'; +import chaiFriendlyPlugin from 'eslint-plugin-chai-friendly'; export default [ { @@ -30,4 +31,15 @@ export default [ 'max-len': ['error', {ignoreRegExpLiterals: true}], }, }, + { + // chai-friendly's rule recognizes chai assertions as non-dead expressions. + files: ['tests/**/*.ts'], + plugins: { + 'chai-friendly': chaiFriendlyPlugin, + }, + rules: { + '@typescript-eslint/no-unused-expressions': 'off', + 'chai-friendly/no-unused-expressions': 'error', + }, + }, ]; diff --git a/frontend/package.json b/frontend/package.json index 39adaf8a9..e60897417 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -43,6 +43,7 @@ "css-loader": "^7.1.4", "css-minimizer-webpack-plugin": "^8.0.0", "eslint": "^10.7.0", + "eslint-plugin-chai-friendly": "^1.2.1", "eslint-plugin-jsdoc": "^63.2.0", "eslint-webpack-plugin": "^6.0.0", "file-loader": "^6.2.0", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index e49f9dd7e..5eba5f16a 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -126,6 +126,9 @@ importers: eslint: specifier: ^10.7.0 version: 10.7.0(jiti@2.7.0) + eslint-plugin-chai-friendly: + specifier: ^1.2.1 + version: 1.2.1(eslint@10.7.0(jiti@2.7.0)) eslint-plugin-jsdoc: specifier: ^63.2.0 version: 63.2.0(eslint@10.7.0(jiti@2.7.0)) @@ -2082,6 +2085,12 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + eslint-plugin-chai-friendly@1.2.1: + resolution: {integrity: sha512-mV3EOJLDr8+L+LS8uCkP711fnNHz+4PsmPyz18xwkvjJwfLRlnx0Eu6CFnb5B+dW5ahoav2jVer2KFYsmIuv3A==} + engines: {node: '>=0.10.0'} + peerDependencies: + eslint: '>=3.0.0' + eslint-plugin-jsdoc@63.2.0: resolution: {integrity: sha512-tccz9igEV5mTKVAwo/KwXqKP7ENqa3a+LpRFuEPAP3k/+SXG4T0sOvA+c2wwTD/TLNrH9MCW4LIu4xEExkJdzg==} engines: {node: ^22.13.0 || >=24} @@ -6066,6 +6075,10 @@ snapshots: escape-string-regexp@4.0.0: {} + eslint-plugin-chai-friendly@1.2.1(eslint@10.7.0(jiti@2.7.0)): + dependencies: + eslint: 10.7.0(jiti@2.7.0) + eslint-plugin-jsdoc@63.2.0(eslint@10.7.0(jiti@2.7.0)): dependencies: '@es-joy/jsdoccomment': 0.88.0 From 06e75c42fae078e07386f25ef83af0dffaa0ec21 Mon Sep 17 00:00:00 2001 From: gusthoff Date: Fri, 24 Jul 2026 20:51:59 +0200 Subject: [PATCH 3/7] Frontend: remove dead variables and imports flagged by eslint - Every *.spec.ts file called chai's use() to register a plugin (chai-dom, chai-as-promised) but captured the return value in an unused `chai` binding; only the side effect of registering the plugin matters, so the binding is dropped. - download.spec.ts imported chai-as-promised but never registered it nor used any of its assertions (no .eventually, .rejectedWith, etc. anywhere in the file) -- removed as genuinely dead. - widget.spec.ts had chai-dom's registration commented out (`// const chai = use(chaiDom);`) even though this file's own tests use the chai-dom `.to.have.class()` matcher; it only worked because Mocha loads spec files alphabetically and every other spec file registers chai-dom first. Restored the registration so this file doesn't silently depend on load order, and dropped its unused `Client` import from mock-socket (never referenced). - Removed two copies of `const identifier = 123;` and one `let receivedMessages: Array = [];` in widget.spec.ts that were declared but never read anywhere in their scope (verified against their sibling blocks, which do use same-named variables for real). - Renamed unused `(event)` callback parameters to `(_event)` in the mock-socket message handlers across server.spec.ts and widget.spec.ts, and added `argsIgnorePattern: '^_'` to the no-unused-vars rule in eslint.config.js -- widget.spec.ts already had one `_event` occurrence that was still being flagged because this option was never set. - Dropped `@typescript-eslint/no-empty-function` from an eslint-disable comment in widget.spec.ts: the rule was never reporting a problem there in the first place. Verified with `pnpm run test`: 126 passing, no change in behavior. Co-Authored-By: Claude Sonnet 5 --- frontend/eslint.config.js | 1 + frontend/tests/ts/areas.spec.ts | 2 +- frontend/tests/ts/dom-utils.spec.ts | 2 +- frontend/tests/ts/download.spec.ts | 3 +-- frontend/tests/ts/editor.spec.ts | 2 +- frontend/tests/ts/scrolltop.spec.ts | 2 +- frontend/tests/ts/server.spec.ts | 8 ++++---- frontend/tests/ts/widget.spec.ts | 19 ++++++++----------- 8 files changed, 18 insertions(+), 21 deletions(-) diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index 6ec0b1ad3..5215b8b3f 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -29,6 +29,7 @@ export default [ ...tsPlugin.configs.recommended.rules, 'jsdoc/no-undefined-types': 'off', 'max-len': ['error', {ignoreRegExpLiterals: true}], + '@typescript-eslint/no-unused-vars': ['error', {argsIgnorePattern: '^_'}], }, }, { diff --git a/frontend/tests/ts/areas.spec.ts b/frontend/tests/ts/areas.spec.ts index 982a01a25..da8693e1c 100644 --- a/frontend/tests/ts/areas.spec.ts +++ b/frontend/tests/ts/areas.spec.ts @@ -2,7 +2,7 @@ import { expect, use } from 'chai'; import chaiDom from 'chai-dom'; -const chai = use(chaiDom); +use(chaiDom); // Import package under test import {Area, OutputArea, LabArea, makeLabArea, LabContainer} diff --git a/frontend/tests/ts/dom-utils.spec.ts b/frontend/tests/ts/dom-utils.spec.ts index f0a22c2c8..8071955be 100644 --- a/frontend/tests/ts/dom-utils.spec.ts +++ b/frontend/tests/ts/dom-utils.spec.ts @@ -1,7 +1,7 @@ // Import testing libs import { expect, use } from 'chai'; import chaiDom from 'chai-dom'; -const chai = use(chaiDom); +use(chaiDom); import {getElemById, getElemsByClass, getElemsByTag} from '../../src/ts/dom-utils'; diff --git a/frontend/tests/ts/download.spec.ts b/frontend/tests/ts/download.spec.ts index 4084638a0..79209cc5a 100644 --- a/frontend/tests/ts/download.spec.ts +++ b/frontend/tests/ts/download.spec.ts @@ -1,8 +1,7 @@ import { expect, use } from 'chai'; import chaiDom from 'chai-dom'; -import chaiAsPromised from 'chai-as-promised'; -const chai = use(chaiDom); +use(chaiDom); import JSZip from 'jszip'; import FileSaver from 'file-saver'; diff --git a/frontend/tests/ts/editor.spec.ts b/frontend/tests/ts/editor.spec.ts index 9c430fa10..a4f3318ec 100644 --- a/frontend/tests/ts/editor.spec.ts +++ b/frontend/tests/ts/editor.spec.ts @@ -2,7 +2,7 @@ import { expect, use } from 'chai'; import chaiDom from 'chai-dom'; -const chai = use(chaiDom); +use(chaiDom); import * as Ace from 'ace-builds'; diff --git a/frontend/tests/ts/scrolltop.spec.ts b/frontend/tests/ts/scrolltop.spec.ts index 8ea0a5beb..97641ecfc 100644 --- a/frontend/tests/ts/scrolltop.spec.ts +++ b/frontend/tests/ts/scrolltop.spec.ts @@ -2,7 +2,7 @@ import { expect, use } from 'chai'; import chaiDom from 'chai-dom'; -const chai = use(chaiDom); +use(chaiDom); import {scrollTop} from '../../src/ts/scrolltop'; diff --git a/frontend/tests/ts/server.spec.ts b/frontend/tests/ts/server.spec.ts index b490fdab1..e08c91426 100644 --- a/frontend/tests/ts/server.spec.ts +++ b/frontend/tests/ts/server.spec.ts @@ -4,7 +4,7 @@ import chaiAsPromised from 'chai-as-promised'; import chaiDom from 'chai-dom'; use(chaiAsPromised); -const chai = use(chaiDom); +use(chaiDom); import {Server, WebSocket} from 'mock-socket'; @@ -66,7 +66,7 @@ describe('ServerWorker', () => { before(() => { server.on('connection', (socket) => { - socket.on('message', (event) => { + socket.on('message', (_event) => { serverResponses.forEach((msg) => { socket.send(JSON.stringify(msg)); }); @@ -95,7 +95,7 @@ describe('ServerWorker', () => { before(() => { server.on('connection', (socket) => { - socket.on('message', (event) => { + socket.on('message', (_event) => { socket.send(JSON.stringify(serverResponse)); }); }); @@ -117,7 +117,7 @@ describe('ServerWorker', () => { before(() => { server.on('connection', (socket) => { - socket.on('message', (event) => {}); + socket.on('message', (_event) => {}); }); }); diff --git a/frontend/tests/ts/widget.spec.ts b/frontend/tests/ts/widget.spec.ts index 6caefc386..35e121d6a 100644 --- a/frontend/tests/ts/widget.spec.ts +++ b/frontend/tests/ts/widget.spec.ts @@ -2,11 +2,11 @@ import { expect, use } from 'chai'; import chaiAsPromised from 'chai-as-promised'; import chaiDom from 'chai-dom'; -import {Client, Server, WebSocket} from 'mock-socket'; +import {Server, WebSocket} from 'mock-socket'; import FileSaver from 'file-saver'; -// const chai = use(chaiDom); -const chai = use(chaiAsPromised); +use(chaiDom); +use(chaiAsPromised); import {readFileSync} from 'fs'; import {resolve} from 'path'; @@ -139,7 +139,6 @@ describe('Widget', () => { let buttonGroup: HTMLElement; let outputDiv: HTMLElement; let runButton: HTMLButtonElement; - const identifier = 123; before(() => { buttonGroup = getElemById(root.id + '.button-group'); @@ -147,7 +146,7 @@ describe('Widget', () => { // stub scrollIntoView function beacuse JSDOM doesn't have it // eslint-disable-next-line max-len - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars window.HTMLElement.prototype.scrollIntoView = (arg: any): void => {}; }); @@ -264,7 +263,6 @@ describe('Widget', () => { let fakeDiv: HTMLDivElement; let fakeOA: OutputArea; let realDiv: HTMLElement; - let receivedMessages: Array = []; beforeEach(() => { fakeDiv = document.createElement('div') as HTMLDivElement; @@ -281,7 +279,7 @@ describe('Widget', () => { "requestId": "abc_-=2" }; server.on('connection', (socket) => { - socket.on('message', (event) => { + socket.on('message', (_event) => { socket.send(JSON.stringify(serverResponse)); }); }); @@ -313,7 +311,7 @@ describe('Widget', () => { } ]; server.on('connection', (socket) => { - socket.on('message', (event) => { + socket.on('message', (_event) => { serverResponses.forEach((msg) => { socket.send(JSON.stringify(msg)); }); @@ -347,7 +345,7 @@ describe('Widget', () => { } ]; server.on('connection', (socket) => { - socket.on('message', (event) => { + socket.on('message', (_event) => { serverResponses.forEach((msg) => { socket.send(JSON.stringify(msg)); }); @@ -384,7 +382,7 @@ describe('Widget', () => { } ]; server.on('connection', (socket) => { - socket.on('message', (event) => { + socket.on('message', (_event) => { serverResponses.forEach((msg) => { socket.send(JSON.stringify(msg)); }); @@ -640,7 +638,6 @@ describe('Widget', () => { }); describe('Normal Behavior', () => { - const identifier = 123; const consoleMsg = 'General message'; let receivedMessages: Array = []; From 5b483f017340533354fdbbaafac9d79c5921ecfa Mon Sep 17 00:00:00 2001 From: gusthoff Date: Fri, 24 Jul 2026 20:57:26 +0200 Subject: [PATCH 4/7] Frontend: wrap long lines flagged by eslint's max-len rule 23 lines across the test spec files exceeded the 80-column limit. Reflowed each with the codebase's existing continuation style (indent the wrapped remainder relative to the statement, or break chained assertions at the dot), and split one nested type assertion into an intermediate id variable + as-cast rather than wrapping across the "as" keyword, which the TypeScript parser used here does not accept split across lines. Verified with `pnpm run test`: 126 passing, no change in behavior. Co-Authored-By: Claude Sonnet 5 --- frontend/tests/ts/download.spec.ts | 27 +++++++++++++++------- frontend/tests/ts/editor.spec.ts | 3 ++- frontend/tests/ts/server.spec.ts | 9 +++++--- frontend/tests/ts/widget.spec.ts | 37 ++++++++++++++++++------------ 4 files changed, 49 insertions(+), 27 deletions(-) diff --git a/frontend/tests/ts/download.spec.ts b/frontend/tests/ts/download.spec.ts index 79209cc5a..48cf2f22b 100644 --- a/frontend/tests/ts/download.spec.ts +++ b/frontend/tests/ts/download.spec.ts @@ -111,7 +111,8 @@ describe('Download', () => { }); it('should find builder switches', () => { - const parsedSwitches = parseSwitches({Builder: ["test1", "test2"], Compiler: []}); + const parsedSwitches = parseSwitches( + {Builder: ["test1", "test2"], Compiler: []}); const expectedBuilder = 'for Switches ("Ada") use ("test1", "test2");'; const expectedCompiler = 'for Switches ("Ada") use ();'; expect(parsedSwitches['--BUILDER_SWITCHES_PLACEHOLDER--']).to.equal( @@ -123,7 +124,8 @@ describe('Download', () => { }); it('should find compiler switches', () => { - const parsedSwitches = parseSwitches({Builder: [], "Compiler": ["test3", "test4"]}); + const parsedSwitches = parseSwitches( + {Builder: [], "Compiler": ["test3", "test4"]}); const expectedBuilder = 'for Switches ("Ada") use ();'; const expectedCompiler = 'for Switches ("Ada") use ("test3", "test4");'; expect(parsedSwitches['--BUILDER_SWITCHES_PLACEHOLDER--']).to.equal( @@ -212,7 +214,11 @@ describe('Download', () => { }); it('should find a main (c file)', () => { - const cFile = `#include \nint main() {\nprintf("Hello, World!");\nreturn 0;\n}`; + const cFile = `#include +int main() { +printf("Hello, World!"); +return 0; +}`; const files: ResourceList = [ {basename: 'other.c', contents: cFile}, {basename: 'test.ads', contents: ''}, @@ -258,13 +264,15 @@ describe('Download', () => { describe('#getGprContents()', () => { it('should replace all placeholders', () => { const files: ResourceList = [{basename: 'main.adb', contents: ''}]; - const gpr = getGprContents(files, {Builder: [], Compiler: []}, 'main.adb', false); + const gpr = + getGprContents(files, {Builder: [], Compiler: []}, 'main.adb', false); expect(gpr).to.not.contain('--MAIN_PLACEHOLDER--'); expect(gpr).to.not.contain('--LANGUAGE_PLACEHOLDER--'); expect(gpr).to.not.contain('--COMPILER_SWITCHES_PLACEHOLDER--'); expect(gpr).to.not.contain('--BUILDER_SWITCHES_PLACEHOLDER--'); expect(gpr).to.not.contain('--'); - const gpr_spark = getGprContents(files, {Builder: [], Compiler: []}, 'main.adb', true); + const gpr_spark = + getGprContents(files, {Builder: [], Compiler: []}, 'main.adb', true); expect(gpr_spark).to.not.contain('--MAIN_PLACEHOLDER--'); expect(gpr_spark).to.not.contain('--LANGUAGE_PLACEHOLDER--'); expect(gpr_spark).to.not.contain('--COMPILER_SWITCHES_PLACEHOLDER--'); @@ -280,7 +288,8 @@ describe('Download', () => { let origGenerateAsync: any; const files: ResourceList = [ - {basename: 'main.adb', contents: 'procedure Main is begin null; end Main;'}, + {basename: 'main.adb', + contents: 'procedure Main is begin null; end Main;'}, ]; const switches: UnparsedSwitches = {Builder: [], Compiler: []}; @@ -312,7 +321,8 @@ describe('Download', () => { expect(savedFilename).to.equal('MyProject.zip'); }); - it('should include source, main.gpr, and main.adc in non-SPARK mode', async () => { + it('should include source, main.gpr, and main.adc in non-SPARK mode', + async () => { downloadProject(files, switches, 'main.adb', 'Test', false); await new Promise((r) => setTimeout(r, 200)); expect(capturedFileNames).to.include('main.adb'); @@ -322,7 +332,8 @@ describe('Download', () => { expect(capturedFileNames).not.to.include('main_spark.adc'); }); - it('should also include main_spark.gpr and main_spark.adc in SPARK mode', async () => { + it('should also include main_spark.gpr and main_spark.adc in SPARK mode', + async () => { downloadProject(files, switches, 'main.adb', 'Test', true); await new Promise((r) => setTimeout(r, 200)); expect(capturedFileNames).to.include('main.gpr'); diff --git a/frontend/tests/ts/editor.spec.ts b/frontend/tests/ts/editor.spec.ts index a4f3318ec..2852fba38 100644 --- a/frontend/tests/ts/editor.spec.ts +++ b/frontend/tests/ts/editor.spec.ts @@ -40,7 +40,8 @@ describe('Editor', () => { }); it('should set C_CPP mode for a .c file', () => { - const cResource: Resource = {basename: 'main.c', contents: 'int main() { return 0; }'}; + const cResource: Resource = + {basename: 'main.c', contents: 'int main() { return 0; }'}; inTest.addSession(cResource.basename, cResource.contents); inTest.setSession(cResource.basename); const session = editor.getSession(); diff --git a/frontend/tests/ts/server.spec.ts b/frontend/tests/ts/server.spec.ts index e08c91426..ee078d4d1 100644 --- a/frontend/tests/ts/server.spec.ts +++ b/frontend/tests/ts/server.spec.ts @@ -36,7 +36,8 @@ describe('ServerWorker', () => { lab: false, }; let server: Server = new Server(baseURL); - let client: ServerWorker = new ServerWorker(baseURL, (data: CheckOutput.FS): boolean => { + let client: ServerWorker = new ServerWorker(baseURL, + (data: CheckOutput.FS): boolean => { cbCount++; return data.completed; }); @@ -106,7 +107,8 @@ describe('ServerWorker', () => { }); it('should throw an exception when AWS rejects the request', async () => { - await expect(client.execute(tsData, 2000)).to.be.rejectedWith(expectedErrorMsg); + await expect(client.execute(tsData, 2000)) + .to.be.rejectedWith(expectedErrorMsg); }); }); @@ -126,7 +128,8 @@ describe('ServerWorker', () => { }); it('should timeout if no response is recieved', async () => { - await expect(client.execute(tsData, timeout)).to.be.rejectedWith(expectedErrorMsg); + await expect(client.execute(tsData, timeout)) + .to.be.rejectedWith(expectedErrorMsg); }); }); }); diff --git a/frontend/tests/ts/widget.spec.ts b/frontend/tests/ts/widget.spec.ts index 35e121d6a..925b0f2f8 100644 --- a/frontend/tests/ts/widget.spec.ts +++ b/frontend/tests/ts/widget.spec.ts @@ -222,7 +222,8 @@ describe('Widget', () => { 'Compiler': ['-gnata', '-gnatX'], }; - const request: RunProgram.TS = JSON.parse(receivedMessages[0]) as RunProgram.TS; + const request: RunProgram.TS = + JSON.parse(receivedMessages[0]) as RunProgram.TS; expect(request.data.files).to.have.length(1); expect(request.data.mode).to.equal('run'); @@ -404,11 +405,13 @@ describe('Widget', () => { root.dataset.switches = 'invalid json'; runButton.click(); await ServerWorker.delay(250); - expect(realDiv.textContent).to.include(Strings.INTERNAL_ERROR_MESSAGE); + expect(realDiv.textContent) + .to.include(Strings.INTERNAL_ERROR_MESSAGE); root.dataset.switches = originalSwitches as string; }); - it('should add output line for stdout from a file not in viewMap', async () => { + it('should add output line for stdout from a file not in viewMap', + async () => { const serverResponse: CheckOutput.FS = { output: [ {type: 'stdout', data: 'unknown.adb:1:2: error: test error'}, @@ -482,8 +485,8 @@ describe('Widget', () => { }); it('should revert theme setting when user cancels', () => { - const themeSetting = getElemById(root.id + '.settings-bar.theme-setting') as - HTMLInputElement; + const themeSetting = getElemById( + root.id + '.settings-bar.theme-setting') as HTMLInputElement; window.confirm = (): boolean => false; const originalChecked = themeSetting.checked; themeSetting.checked = !originalChecked; @@ -492,8 +495,8 @@ describe('Widget', () => { }); it('should apply dark theme and reload when user confirms', () => { - const themeSetting = getElemById(root.id + '.settings-bar.theme-setting') as - HTMLInputElement; + const themeSetting = getElemById( + root.id + '.settings-bar.theme-setting') as HTMLInputElement; window.confirm = (): boolean => true; themeSetting.checked = true; try { @@ -511,11 +514,12 @@ describe('Widget', () => { getElemById(root.id + '.settings-bar.compiler-switches'); }); - it('should deactivate mutually exclusive switches when one is checked', () => { - const gnato = document.getElementById( - root.id + '.settings-bar.compiler-switches.-gnato') as HTMLInputElement; - const gnato0 = document.getElementById( - root.id + '.settings-bar.compiler-switches.-gnato0') as HTMLInputElement; + it('should deactivate mutually exclusive switches when one is checked', + () => { + const gnatoId = root.id + '.settings-bar.compiler-switches.-gnato'; + const gnato = document.getElementById(gnatoId) as HTMLInputElement; + const gnato0Id = root.id + '.settings-bar.compiler-switches.-gnato0'; + const gnato0 = document.getElementById(gnato0Id) as HTMLInputElement; gnato.checked = true; gnato0.checked = true; triggerEvent(gnato0, 'change'); @@ -547,7 +551,8 @@ describe('Widget', () => { 'compiler-switch-help-info')[0] as HTMLElement; const firstEntry = compilerSwitchesSetting.getElementsByClassName( 'compiler-switch-entry')[0] as HTMLElement; - const b = firstEntry.getElementsByTagName('button')[0] as HTMLButtonElement; + const b = + firstEntry.getElementsByTagName('button')[0] as HTMLButtonElement; b.click(); expect(d.classList.contains('disabled')).to.be.false; expect(d.querySelector('b')).to.not.be.null; @@ -582,7 +587,8 @@ describe('Widget', () => { const outputDiv = getElemById(root.id + '.output-area'); dlButton.click(); await ServerWorker.delay(100); - expect(outputDiv.textContent).to.include(Strings.INTERNAL_ERROR_MESSAGE); + expect(outputDiv.textContent) + .to.include(Strings.INTERNAL_ERROR_MESSAGE); root.dataset.switches = originalSwitches as string; }); }); @@ -781,7 +787,8 @@ describe('Widget', () => { }); it('should populate the code-block-info output element', () => { - const cbiContents = getElemById(root.id + '.code_block_info.run info.contents'); + const cbiContents = + getElemById(root.id + '.code_block_info.run info.contents'); expect(cbiContents.innerText).to.include('Hello from run output!'); }); }); From 788c1901dfaf429e02728505d84ad226d56f445e Mon Sep 17 00:00:00 2001 From: gusthoff Date: Fri, 24 Jul 2026 20:58:47 +0200 Subject: [PATCH 5/7] Frontend: suppress no-explicit-any for a JSZip internals stub download.spec.ts stubs JSZip.prototype.generateAsync to capture the list of files being zipped, and already suppresses no-explicit-any at the two other (JSZip.prototype as any) casts used to install and type that stub -- JSZip's public TypeScript types don't expose this internal method. The after() hook that restores the original function used the same cast but was missing the matching suppression comment. Co-Authored-By: Claude Sonnet 5 --- frontend/tests/ts/download.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/tests/ts/download.spec.ts b/frontend/tests/ts/download.spec.ts index 48cf2f22b..1e4a5e9fd 100644 --- a/frontend/tests/ts/download.spec.ts +++ b/frontend/tests/ts/download.spec.ts @@ -307,6 +307,7 @@ return 0; }); after(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any (JSZip.prototype as any).generateAsync = origGenerateAsync; }); From 13b269cee8e8b19ed7730f36787466aa3ff6fe0b Mon Sep 17 00:00:00 2001 From: gusthoff Date: Fri, 24 Jul 2026 20:59:22 +0200 Subject: [PATCH 6/7] Frontend: fix jsdoc formatting flagged by eslint-plugin-jsdoc - resource.ts: dropped the @export tag from two type declarations. @export is a Closure Compiler annotation, not a recognized JSDoc/TSDoc tag, and is redundant here anyway since TypeScript's own `export` keyword already marks these types as exported. - scrolltop.spec.ts, server.spec.ts, widget.spec.ts: removed blank comment lines between a function's description and its @param tags (jsdoc/tag-lines wants none), added missing @param descriptions on scrollTo's x/y parameters, and fixed two copies of the removeListeners doc comment whose `*` continuation lines were missing their leading space (jsdoc/check-alignment). Co-Authored-By: Claude Sonnet 5 --- frontend/src/ts/resource.ts | 4 ---- frontend/tests/ts/scrolltop.spec.ts | 6 ++---- frontend/tests/ts/server.spec.ts | 7 +++---- frontend/tests/ts/widget.spec.ts | 9 +++------ 4 files changed, 8 insertions(+), 18 deletions(-) diff --git a/frontend/src/ts/resource.ts b/frontend/src/ts/resource.ts index 0e2929327..fc783fd2d 100644 --- a/frontend/src/ts/resource.ts +++ b/frontend/src/ts/resource.ts @@ -1,7 +1,5 @@ /** * Corresponds to a text file - * - * @export */ export type Resource = { basename: string; @@ -10,7 +8,5 @@ export type Resource = { /** * Corresponds to a list of Resources - * - * @export */ export type ResourceList = Array; diff --git a/frontend/tests/ts/scrolltop.spec.ts b/frontend/tests/ts/scrolltop.spec.ts index 97641ecfc..a5e093e60 100644 --- a/frontend/tests/ts/scrolltop.spec.ts +++ b/frontend/tests/ts/scrolltop.spec.ts @@ -8,7 +8,6 @@ import {scrollTop} from '../../src/ts/scrolltop'; /** * Helper function to trigger window event - * * @param {Window} element - The window element * @param {string} eventName - The event to do */ @@ -20,9 +19,8 @@ function triggerEvent(element: Window, eventName: string): void { /** * Helper function used to override default non implemented version in JSDOM - * - * @param {number} x - * @param {number} y + * @param {number} x - The x-coordinate to scroll to + * @param {number} y - The y-coordinate to scroll to */ function scrollTo(x: number, y: number): void { document.body.scrollTop = y; diff --git a/frontend/tests/ts/server.spec.ts b/frontend/tests/ts/server.spec.ts index ee078d4d1..cdf7d597f 100644 --- a/frontend/tests/ts/server.spec.ts +++ b/frontend/tests/ts/server.spec.ts @@ -14,10 +14,9 @@ import {CheckOutput, RunProgram} from '../../src/ts/server-types'; global.WebSocket = WebSocket as unknown as typeof globalThis.WebSocket; /** -* Remove all event listeners from the server -* -* @param {Server} server - The server to remove the listeners from -*/ + * Remove all event listeners from the server + * @param {Server} server - The server to remove the listeners from + */ function removeListeners(server: Server): void { for (let type in server.listeners) { server.listeners[type] = []; diff --git a/frontend/tests/ts/widget.spec.ts b/frontend/tests/ts/widget.spec.ts index 925b0f2f8..dc22098fb 100644 --- a/frontend/tests/ts/widget.spec.ts +++ b/frontend/tests/ts/widget.spec.ts @@ -32,7 +32,6 @@ const __dirname = dirname(__filename); /** * Helper function to fill DOM from a file - * * @param {string} filename - The filename to use */ function fillDOM(filename: string): void { @@ -53,7 +52,6 @@ function clearDOM(): void { /** * Helper function to trigger an event - * * @param {HTMLElement} element - The element to trigger the event on * @param {string} eventName - The event name to trigger */ @@ -64,10 +62,9 @@ function triggerEvent(element: HTMLElement, eventName: string): void { } /** -* Remove all event listeners from the server -* -* @param {Server} server - The server to remove the listeners from -*/ + * Remove all event listeners from the server + * @param {Server} server - The server to remove the listeners from + */ function removeListeners(server: Server): void { for (let type in server.listeners) { server.listeners[type] = []; From a35933299873a6fdf501ead3bc15e60b92f7d926 Mon Sep 17 00:00:00 2001 From: gusthoff Date: Fri, 24 Jul 2026 21:10:35 +0200 Subject: [PATCH 7/7] CI: add a dedicated ESLint workflow pnpm run eslint was never wired into any CI workflow, so lint regressions could accumulate indefinitely without anyone noticing (this is exactly how frontend/eslint.config.js ended up silently linting generated build output, and how 71 real errors piled up in the test spec files). Added as its own workflow rather than a step in typescript-tests.js.yml: that workflow's setup installs the full GNAT/gnatprove/gprbuild toolchain and builds the Sphinx HTML test fixtures before any test runs, none of which ESLint needs. A dedicated workflow mirrors the existing code-projects-type-check.yml (pyright) -- a fast, narrow static-analysis check that fails in seconds and is unambiguous about what broke. Verified locally: `pnpm install --frozen-lockfile && pnpm run eslint` exits 0 against the current lockfile and source tree. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/eslint.js.yml | 37 +++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 .github/workflows/eslint.js.yml diff --git a/.github/workflows/eslint.js.yml b/.github/workflows/eslint.js.yml new file mode 100644 index 000000000..85889bac7 --- /dev/null +++ b/.github/workflows/eslint.js.yml @@ -0,0 +1,37 @@ +name: ESLint + +on: + push: + paths: + - 'frontend/**/*.ts' + pull_request: + branches: + - main + +defaults: + run: + working-directory: frontend + +jobs: + eslint: + + runs-on: ubuntu-24.04 + + strategy: + matrix: + node-version: [24.x] + + steps: + - uses: actions/checkout@v4 + - name: Enable Corepack + run: corepack enable + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'pnpm' + cache-dependency-path: 'frontend/pnpm-lock.yaml' + - name: Install dependencies + run: pnpm install --frozen-lockfile + - name: Run ESLint + run: pnpm run eslint