From 90dbd32a0da3da3170ed5a237f2eac39e9d3dada Mon Sep 17 00:00:00 2001 From: Adam Wright Date: Tue, 13 Oct 2026 12:01:58 +0000 Subject: [PATCH 1/3] Fetch the citation dialog on first open; hold the embed to a fixed budget (closes #356) The embeddable diagram provides CitationService but never shows its dialog, and the service's MatDialog and CitationComponent imports carried Material's dialog code into the embed's first load. They are now imported when the dialog is first opened; the viewport reports a failed fetch. The embed's initial load goes from 1,366 KB to 1,324 KB. Writing a test for the dialog (e2e/citation-dialog.spec.ts; nothing tested it) found the "Cite us" button had no accessible name -- only a tooltip -- so a screen reader announced an unnamed button. It now has aria-label "Cite us", and the test finds it by that name (it fails without it). The citation request carries today's date, so the test answers it in the service's own shape. SC-004 named "less than a third of the full site", a ratio that moves when the site changes: slimming the site raised the bar by about 50 KB while the embed was being slimmed. It now names an absolute budget, set just above today's 1.36 MB initial so the embed can only shrink: warn 1.4 MB, error 1.45 MB (angular.json, production and beta). Checked: a 1.3 MB budget fails the build. Co-Authored-By: Claude Opus 5.5 --- angular.json | 8 ++-- e2e/citation-dialog.spec.ts | 39 +++++++++++++++++++ ...r-the-pathway-on-screen-and-clo-96a52b.har | 1 + .../src/app/services/citation.service.ts | 17 +++++--- .../src/app/viewport/viewport.component.html | 8 +++- .../src/app/viewport/viewport.component.ts | 5 ++- specs/009-embeddable-diagram/spec.md | 3 +- specs/009-embeddable-diagram/tasks.md | 1 + 8 files changed, 69 insertions(+), 13 deletions(-) create mode 100644 e2e/citation-dialog.spec.ts create mode 100644 e2e/har/citation-dialog--cite-us-opens-the-citation-for-the-pathway-on-screen-and-clo-96a52b.har diff --git a/angular.json b/angular.json index ad270c26..4cf5a629 100644 --- a/angular.json +++ b/angular.json @@ -356,8 +356,8 @@ "budgets": [ { "type": "initial", - "maximumWarning": "1.65MB", - "maximumError": "1.8MB" + "maximumWarning": "1.4MB", + "maximumError": "1.45MB" } ] }, @@ -369,8 +369,8 @@ "budgets": [ { "type": "initial", - "maximumWarning": "1.65MB", - "maximumError": "1.8MB" + "maximumWarning": "1.4MB", + "maximumError": "1.45MB" } ] }, diff --git a/e2e/citation-dialog.spec.ts b/e2e/citation-dialog.spec.ts new file mode 100644 index 00000000..c35ec52d --- /dev/null +++ b/e2e/citation-dialog.spec.ts @@ -0,0 +1,39 @@ +import { test, expect } from './support/backend'; + +/** + * "Cite us" opens the citation for the pathway on screen. + * + * The dialog and its component are fetched when it is first opened rather than + * with the page (#356), so the first click has something to wait for. Nothing + * tested the dialog before; writing this found the button had no name. + */ +test('Cite us opens the citation for the pathway on screen, and closes', async ({ page }) => { + const thrown: string[] = []; + page.on('pageerror', (error) => thrown.push(String(error))); + // The request carries today's date (?dateAccessed=...), so no recording can + // match it; answered here in the service's own shape. + await page.route('**/ContentService/citation/pathway/R-HSA-69620?*', (route) => + route.fulfill({ + contentType: 'application/json', + body: JSON.stringify({ + pathwayCitation: + "Yen, T.J. ,Khanna, K.K. ,Hoffmann, I. ,Walworth, N. & O'Donnell, M. (2005). Cell Cycle Checkpoints. Reactome, 97, https://reactome.org/content/detail/R-HSA-69620", + imageCitation: + 'Image Citation for Cell Cycle Checkpoints. Reactome, 97, https://reactome.org/content/detail/R-HSA-69620', + }), + }) + ); + await page.goto('/PathwayBrowser/R-HSA-69620'); + // By its name: it had none, only a tooltip, so a screen reader announced an + // unnamed button. + await page.getByRole('button', { name: 'Cite us' }).click({ timeout: 60_000 }); + const dialog = page.locator('cr-citation'); + await expect(dialog.getByRole('heading', { name: 'Cite Us!' })).toBeVisible({ timeout: 30_000 }); + await expect(dialog.getByRole('heading', { name: 'Pathway' })).toBeVisible({ timeout: 30_000 }); + await expect(dialog.locator('mat-dialog-content')).toContainText( + '(2005). Cell Cycle Checkpoints' + ); + await dialog.locator('.close-button').click(); + await expect(dialog).toHaveCount(0); + expect(thrown).toEqual([]); +}); diff --git a/e2e/har/citation-dialog--cite-us-opens-the-citation-for-the-pathway-on-screen-and-clo-96a52b.har b/e2e/har/citation-dialog--cite-us-opens-the-citation-for-the-pathway-on-screen-and-clo-96a52b.har new file mode 100644 index 00000000..766a4cd1 --- /dev/null +++ b/e2e/har/citation-dialog--cite-us-opens-the-citation-for-the-pathway-on-screen-and-clo-96a52b.har @@ -0,0 +1 @@ +{"log":{"version":"1.2","entries":[{"request":{"method":"GET","url":"http://localhost:4330/ContentService/data/database/version"},"response":{"status":200,"headers":[{"name":"Vary","value":"Origin"},{"name":"access-control-allow-origin","value":"*"},{"name":"connection","value":"close"},{"name":"content-length","value":"2"},{"name":"content-type","value":"text/plain;charset=UTF-8"},{"name":"date","value":"Tue, 13 Oct 2026 11:56:58 GMT"}],"content":{"mimeType":"text/plain;charset=UTF-8","_file":"812ed4562d3211363a7b813aa9cd2cf042b63bb2.txt"}}},{"request":{"method":"GET","url":"http://localhost:4330/ContentService/data/species/main"},"response":{"status":200,"headers":[{"name":"Vary","value":"Origin"},{"name":"access-control-allow-origin","value":"*"},{"name":"connection","value":"close"},{"name":"content-length","value":"3237"},{"name":"content-type","value":"application/json; charset=utf-8"},{"name":"date","value":"Tue, 13 Oct 2026 11:56:58 GMT"},{"name":"etag","value":"W/\"ca5-sa7k1xX74ycTbey3jmBLCrhGTO0\""}],"content":{"mimeType":"application/json; charset=utf-8","_file":"b1aee4d715fbe327136decb78e604b0ab8464ced.json"}}},{"request":{"method":"GET","url":"http://localhost:4330/ContentService/data/query/enhanced/v2/R-HSA-69620?fetchIncomingRelationships=true&summariseReferenceEntity=false&includeDisease=true&includeRef=true&view=nested-aggregated"},"response":{"status":200,"headers":[{"name":"Transfer-Encoding","value":"chunked"},{"name":"Vary","value":"Origin"},{"name":"access-control-allow-origin","value":"*"},{"name":"connection","value":"close"},{"name":"content-type","value":"application/json;charset=UTF-8"},{"name":"date","value":"Tue, 13 Oct 2026 11:56:58 GMT"}],"content":{"mimeType":"application/json;charset=UTF-8","_file":"e71f7705e50fdd572ddc0cf27a5b6ed24c562e03.json"}}},{"request":{"method":"GET","url":"http://localhost:4330/ContentService/data/event/R-HSA-69620/ancestors?includeRef=true&view=nested-aggregated"},"response":{"status":200,"headers":[{"name":"Vary","value":"Origin"},{"name":"access-control-allow-origin","value":"*"},{"name":"connection","value":"close"},{"name":"content-length","value":"696"},{"name":"content-type","value":"application/json;charset=UTF-8"},{"name":"date","value":"Tue, 13 Oct 2026 11:56:59 GMT"}],"content":{"mimeType":"application/json;charset=UTF-8","_file":"77b1f4b5f0865f10a4a061fe36168fd1f62e604b.json"}}},{"request":{"method":"GET","url":"http://localhost:4330/ContentService/data/query/enhanced/v2/R-HSA-69620?fetchIncomingRelationships=true&summariseReferenceEntity=true&includeDisease=true&includeRef=true&view=nested-aggregated"},"response":{"status":200,"headers":[{"name":"Transfer-Encoding","value":"chunked"},{"name":"Vary","value":"Origin"},{"name":"access-control-allow-origin","value":"*"},{"name":"connection","value":"close"},{"name":"content-type","value":"application/json;charset=UTF-8"},{"name":"date","value":"Tue, 13 Oct 2026 11:56:58 GMT"}],"content":{"mimeType":"application/json;charset=UTF-8","_file":"e71f7705e50fdd572ddc0cf27a5b6ed24c562e03.json"}}},{"request":{"method":"GET","url":"http://localhost:4330/ExperimentDigester/experiments/summaries"},"response":{"status":200,"headers":[{"name":"Transfer-Encoding","value":"chunked"},{"name":"Vary","value":"Origin"},{"name":"connection","value":"close"},{"name":"content-type","value":"application/json;charset=UTF-8"},{"name":"date","value":"Tue, 13 Oct 2026 11:56:58 GMT"}],"content":{"mimeType":"application/json;charset=UTF-8","_file":"5e25288a19ddb81e5e80ee5d8aa56f19cd68dd6e.json"}}},{"request":{"method":"GET","url":"http://localhost:4330/ContentService/data/pathways/top/9606"},"response":{"status":200,"headers":[{"name":"Transfer-Encoding","value":"chunked"},{"name":"Vary","value":"Origin"},{"name":"access-control-allow-origin","value":"*"},{"name":"connection","value":"close"},{"name":"content-type","value":"application/json;charset=UTF-8"},{"name":"date","value":"Tue, 13 Oct 2026 11:56:58 GMT"}],"content":{"mimeType":"application/json;charset=UTF-8","_file":"28a2743b73a578fbfc87d6ec212f2dababf479e3.json"}}},{"request":{"method":"GET","url":"http://localhost:4330/ContentService/interactors/psicquic/resources/"},"response":{"status":200,"headers":[{"name":"Vary","value":"Origin"},{"name":"access-control-allow-origin","value":"*"},{"name":"connection","value":"close"},{"name":"content-length","value":"6674"},{"name":"content-type","value":"application/json;charset=UTF-8"},{"name":"date","value":"Tue, 13 Oct 2026 11:56:58 GMT"}],"content":{"mimeType":"application/json;charset=UTF-8","_file":"0e84a258a3b413badb1205d66079ecb7ea99bbd0.json"}}},{"request":{"method":"GET","url":"http://localhost:4330/ContentService/data/query/enhanced/v2/R-HSA-69620?includeRef=true&view=nested-aggregated&summariseReferenceEntity=false&includeDisease=true"},"response":{"status":200,"headers":[{"name":"Transfer-Encoding","value":"chunked"},{"name":"Vary","value":"Origin"},{"name":"access-control-allow-origin","value":"*"},{"name":"connection","value":"close"},{"name":"content-type","value":"application/json;charset=UTF-8"},{"name":"date","value":"Tue, 13 Oct 2026 11:56:58 GMT"}],"content":{"mimeType":"application/json;charset=UTF-8","_file":"e71f7705e50fdd572ddc0cf27a5b6ed24c562e03.json"}}},{"request":{"method":"GET","url":"http://localhost:4330/ContentService/data/detail/R-HSA-69620/locationsInPWB"},"response":{"status":200,"headers":[{"name":"Vary","value":"Origin"},{"name":"access-control-allow-origin","value":"*"},{"name":"connection","value":"close"},{"name":"content-length","value":"284"},{"name":"content-type","value":"application/json;charset=UTF-8"},{"name":"date","value":"Tue, 13 Oct 2026 11:56:58 GMT"}],"content":{"mimeType":"application/json;charset=UTF-8","_file":"f54e01541057b3966e2f3122f73577bb12bb7d64.json"}}},{"request":{"method":"GET","url":"http://localhost:4330/ContentService/data/event/R-HSA-69620/ancestors"},"response":{"status":200,"headers":[{"name":"Vary","value":"Origin"},{"name":"access-control-allow-origin","value":"*"},{"name":"connection","value":"close"},{"name":"content-length","value":"696"},{"name":"content-type","value":"application/json;charset=UTF-8"},{"name":"date","value":"Tue, 13 Oct 2026 11:56:59 GMT"}],"content":{"mimeType":"application/json;charset=UTF-8","_file":"77b1f4b5f0865f10a4a061fe36168fd1f62e604b.json"}}},{"request":{"method":"GET","url":"http://localhost:4330/ContentService/data/query/enhanced/v2/R-HSA-1640170?includeRef=true&view=nested-aggregated&summariseReferenceEntity=false&includeDisease=true"},"response":{"status":200,"headers":[{"name":"Transfer-Encoding","value":"chunked"},{"name":"Vary","value":"Origin"},{"name":"access-control-allow-origin","value":"*"},{"name":"connection","value":"close"},{"name":"content-type","value":"application/json;charset=UTF-8"},{"name":"date","value":"Tue, 13 Oct 2026 11:57:01 GMT"}],"content":{"mimeType":"application/json;charset=UTF-8","_file":"a8afa20987171d392bfd782043fe5594e9842a18.json"}}}]}} \ No newline at end of file diff --git a/projects/pathway-browser/src/app/services/citation.service.ts b/projects/pathway-browser/src/app/services/citation.service.ts index 566d6094..5a8f6301 100644 --- a/projects/pathway-browser/src/app/services/citation.service.ts +++ b/projects/pathway-browser/src/app/services/citation.service.ts @@ -1,4 +1,4 @@ -import { computed, inject, Injectable, Signal, signal } from '@angular/core'; +import { computed, inject, Injectable, Injector, Signal, signal } from '@angular/core'; import { Observable } from 'rxjs'; import { HttpClient } from '@angular/common/http'; import { environment } from '../../environments/environment'; @@ -6,8 +6,6 @@ import { isPathway } from './utils'; import { DataStateService } from './data-state.service'; import { AnalysisService } from './analysis.service'; import { rxResource } from '@angular/core/rxjs-interop'; -import { CitationComponent } from '../citation/citation.component'; -import { MatDialog } from '@angular/material/dialog'; interface PathwayCitation { imageCitation: string; @@ -46,7 +44,10 @@ export class CitationService { private dataState = inject(DataStateService); private analysis = inject(AnalysisService); - readonly dialog = inject(MatDialog); + // The dialog and its component are fetched when it is first opened, not with + // the page: the embeddable diagram provides this service but never shows the + // dialog, and with them it carried Material's dialog code for nothing (#356). + private readonly injector = inject(Injector); readonly currentCitationId = signal(undefined); readonly currentCitationExportURLS = computed(() => @@ -96,9 +97,13 @@ export class CitationService { : this.http.get(`${pathwayUrl}`); } - openDialog() { + async openDialog(): Promise { this.currentCitationId.set(this.updatedCitationId()); - const dialogRef = this.dialog.open(CitationComponent, { + const [{ MatDialog }, { CitationComponent }] = await Promise.all([ + import('@angular/material/dialog'), + import('../citation/citation.component'), + ]); + const dialogRef = this.injector.get(MatDialog).open(CitationComponent, { data: { content: this.citationData.value, id: this.currentCitationId, diff --git a/projects/pathway-browser/src/app/viewport/viewport.component.html b/projects/pathway-browser/src/app/viewport/viewport.component.html index ca2c241d..3c270cb4 100644 --- a/projects/pathway-browser/src/app/viewport/viewport.component.html +++ b/projects/pathway-browser/src/app/viewport/viewport.component.html @@ -336,7 +336,13 @@ [matTooltip]="dark.isDark() ? 'Switch to light mode' : 'Switch to dark mode'" id="dark-toggle" > -