content: integrate approved project evidence
This commit is contained in:
@@ -0,0 +1,210 @@
|
|||||||
|
# Task 6 report — approved media integration and presentation
|
||||||
|
|
||||||
|
Status: **DONE**
|
||||||
|
|
||||||
|
Task 6 attaches the exact seven Task 5-approved project/social assets, restores full manifest/content/file reference closure, keeps Footy Tracker text-only because broadcast display rights remain unconfirmed, and fixes two high-impact presentation defects found during real-media inspection.
|
||||||
|
|
||||||
|
## Published reference closure
|
||||||
|
|
||||||
|
The public projection now references exactly the seven newly approved IDs:
|
||||||
|
|
||||||
|
1. `medication-mobile-logs`
|
||||||
|
2. `medication-mobile-data-settings`
|
||||||
|
3. `medication-mobile-notification-settings`
|
||||||
|
4. `opinion-polarization-chart`
|
||||||
|
5. `opinion-final-distribution-chart`
|
||||||
|
6. `currency-converter-interface`
|
||||||
|
7. `social-preview-nightdrive`
|
||||||
|
|
||||||
|
Together with the unchanged eight soundtrack references, public content contains 15 unique references and the public manifest contains the same 15 IDs. `npm run verify:approved-assets` now exits successfully, closing the documented Task 5/Task 6 seven-ID reference boundary.
|
||||||
|
|
||||||
|
- The medication tracker uses the reviewed three-image `portrait-strip` in activity/history, data/synchronization, reminder/notification order.
|
||||||
|
- Opinion Dynamics uses the two reviewed charts in `chart-pair` order. Only the source-supported polarization values (`0 → 0.765`, `14 → 0.932`) are tabulated. No values were inferred from final-distribution pixel positions.
|
||||||
|
- Currency Converter uses the single reviewed interface image.
|
||||||
|
- `social-preview-nightdrive` is the public metadata preview reference.
|
||||||
|
- Footy Tracker retains an empty `assetIds` array and omits `media`/`mediaLayout`. The checklist records the broadcast frame as deliberately omitted because display rights are unconfirmed.
|
||||||
|
|
||||||
|
## TDD evidence
|
||||||
|
|
||||||
|
### Approved content references
|
||||||
|
|
||||||
|
RED:
|
||||||
|
|
||||||
|
```text
|
||||||
|
npm run test:unit -- tests/integration/public-content.test.ts
|
||||||
|
# 1 file failed; 3/3 tests failed
|
||||||
|
# New assertion received medication assetIds: [] instead of the reviewed portrait-strip IDs.
|
||||||
|
# Existing production validation also reported the expected Task 5 boundary:
|
||||||
|
# Unreferenced manifest asset: currency-converter-interface
|
||||||
|
```
|
||||||
|
|
||||||
|
GREEN after attaching the exact reviewed presentations:
|
||||||
|
|
||||||
|
```text
|
||||||
|
npm run test:unit -- tests/integration/public-content.test.ts tests/unit/ProjectMedia.test.ts
|
||||||
|
# 2 files passed; 15/15 tests passed
|
||||||
|
```
|
||||||
|
|
||||||
|
The concrete gallery regression renders the real approved public records and verifies six project stills, intrinsic dimensions, lazy loading, asynchronous decoding, contextual medication descriptions, two visible chart descriptions, and exactly one exact-value table.
|
||||||
|
|
||||||
|
### Cross-task compatibility rulings
|
||||||
|
|
||||||
|
Attaching `socialPreviewAssetId` correctly activated the existing Open Graph image implementation, exposing three stale Task 7-era test assumptions during the first `build:preview` run:
|
||||||
|
|
||||||
|
```text
|
||||||
|
tests/unit/metadata.test.ts expected metadata.openGraph.image to be undefined
|
||||||
|
tests/integration/prerender.test.ts expected zero og:image elements
|
||||||
|
tests/unit/entry-client.test.ts changed assetIds without changing exact media closure
|
||||||
|
```
|
||||||
|
|
||||||
|
The controller extended Task 6 ownership narrowly to those tests. Their assertions now derive the approved preview URL from the manifest, and the entry-client negative fixture mutates the matching presentation and declaration together so it still reaches the intended unmanifested-reference boundary. No Task 7 Twitter, structured-location, metadata production, or artifact-verifier work was pulled forward.
|
||||||
|
|
||||||
|
Task 3's synthetic responsive fixtures also replaced the medication references while retaining the newly referenced approved medication records, creating expected unreferenced-manifest failures. The controller extended ownership narrowly to the two fixture files. They now remove exactly the three superseded medication manifest records before adding synthetic stress records; no unrelated approved record is removed and `validateProductionInputs` remains strict. The mixed-aspect chart assertion is scoped to its synthetic Footy fixture so the new real Opinion pair does not make the locator ambiguous.
|
||||||
|
|
||||||
|
### Real chart and print defects
|
||||||
|
|
||||||
|
Real-media review found that the chart description and value table were both placed in grid row 3 with automatic columns. RED browser evidence at 1440px measured:
|
||||||
|
|
||||||
|
```text
|
||||||
|
description grid column/row: auto / 3
|
||||||
|
table grid column/row: auto / 3
|
||||||
|
description width ratio: 0.510
|
||||||
|
table width ratio: 0.344
|
||||||
|
Opinion article size: 609 × 6010 CSS px
|
||||||
|
```
|
||||||
|
|
||||||
|
The same RED pass found print project figures retaining the screen surface `rgba(15, 28, 43, 0.86)` while print text became dark, making captions and evidence text unreadable.
|
||||||
|
|
||||||
|
The minimal fixes assign the chart description to column 1 / row 3 and the table to column 1 / row 4, and use a white, dark-text project-media surface only in print. GREEN evidence:
|
||||||
|
|
||||||
|
```text
|
||||||
|
npx playwright test tests/browser/media-policy.spec.ts \
|
||||||
|
--project=desktop-chromium --workers=1 --grep "desktop chart|print project"
|
||||||
|
# 2/2 passed
|
||||||
|
|
||||||
|
description/table width ratio: 0.855 each (the full padded content column)
|
||||||
|
Opinion article size: 609 × 2271 CSS px
|
||||||
|
print media background: rgb(255, 255, 255)
|
||||||
|
minimum tested text contrast: at least 4.5:1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Browser policy and failure behavior
|
||||||
|
|
||||||
|
- Six approved project stills render from manifest URLs with exact intrinsic dimensions, `loading="lazy"`, and `decoding="async"`.
|
||||||
|
- No public project video or video source is attached or requested in normal, Static, reduced-motion, or F8 failure modes.
|
||||||
|
- The private medication tracker has no public link.
|
||||||
|
- Aborted project-image requests leave all six image elements, meaningful alt text, medication captions/long descriptions, both chart conclusions, exact values, project titles, and surrounding text in the document.
|
||||||
|
- F8 remains scoped to `video, video source`; approved still figures remain valid during the video-failure scenario.
|
||||||
|
- Visual regression now derives its expected six project stills from public content. No visual baseline was updated because the suite records/asserts live composition rather than accepting changed golden pixels.
|
||||||
|
|
||||||
|
## Visual inspection
|
||||||
|
|
||||||
|
A temporary inspection harness built the real final projection and passed 6/6 cases. It checked six loaded images, no document-level horizontal overflow, figures contained by their articles, text-only Footy, all captions, and failed-image text equivalents, then captured article-level evidence.
|
||||||
|
|
||||||
|
Reviewed states:
|
||||||
|
|
||||||
|
- 1440×900 desktop Static
|
||||||
|
- 320×844 narrow mobile Static
|
||||||
|
- 720 CSS-pixel viewport as the 200%-zoom-equivalent reflow inspection for a 1440px desktop
|
||||||
|
- 390×844 reduced motion
|
||||||
|
- 1440×900 print media
|
||||||
|
- 1440×900 failed-image fallback
|
||||||
|
|
||||||
|
Representative retained artifacts:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/tmp/portfolio-task-6-visual/desktop-static-medication.png
|
||||||
|
/tmp/portfolio-task-6-visual/desktop-static-opinion.png
|
||||||
|
/tmp/portfolio-task-6-visual/desktop-static-currency.png
|
||||||
|
/tmp/portfolio-task-6-visual/mobile-static-medication.png
|
||||||
|
/tmp/portfolio-task-6-visual/mobile-static-opinion.png
|
||||||
|
/tmp/portfolio-task-6-visual/zoom-200-equivalent-medication.png
|
||||||
|
/tmp/portfolio-task-6-visual/zoom-200-equivalent-opinion.png
|
||||||
|
/tmp/portfolio-task-6-visual/mobile-reduced-motion-medication.png
|
||||||
|
/tmp/portfolio-task-6-visual/mobile-reduced-motion-opinion.png
|
||||||
|
/tmp/portfolio-task-6-visual/desktop-print-medication.png
|
||||||
|
/tmp/portfolio-task-6-visual/desktop-print-opinion.png
|
||||||
|
/tmp/portfolio-task-6-visual/failed-image-medication.png
|
||||||
|
/tmp/portfolio-task-6-visual/failed-image-opinion.png
|
||||||
|
```
|
||||||
|
|
||||||
|
Findings after repair:
|
||||||
|
|
||||||
|
- Medication portraits remain uncropped and readable, retain their reviewed order, collapse to a simple single-column sequence on narrow/zoom-equivalent layouts, and fit print pages without overlap.
|
||||||
|
- Opinion charts preserve their full plot pixels, aligned desktop pair, sequential narrow layout, visible variables/trend/conclusion text, and readable exact table. The pathological desktop height and character-width table wrapping are gone.
|
||||||
|
- Currency remains contained at desktop and mobile; its caption and long description explain the workflow rather than colors.
|
||||||
|
- Static and reduced-motion presentations retain identical project evidence and introduce no video behavior.
|
||||||
|
- Print captions, descriptions, and exact tables are dark-on-white and readable; screenshots remain visible and contained.
|
||||||
|
- Failed images reserve their frames and expose contextual alt text while adjacent captions, descriptions, exact values, and project evidence remain readable.
|
||||||
|
- No further high-impact crop, hierarchy, caption, contrast, or overflow defect remained after the two scoped CSS repairs.
|
||||||
|
|
||||||
|
## Privacy and approval integrity
|
||||||
|
|
||||||
|
- No approved media bytes or manifest hashes changed in Task 6.
|
||||||
|
- The medication tracker remains generic, private, URL-free, and described only through fictional/demo media states.
|
||||||
|
- The final-distribution chart contains no inferred exact values.
|
||||||
|
- Footy third-party footage remains unpublished.
|
||||||
|
- The public checklist contains exact approved IDs and the Footy omission decision, with no private source paths.
|
||||||
|
- No private tracker URL, repository path, product name, source mapping, denylist content, source screenshot path, or approval-register path was added to the public projection or tests.
|
||||||
|
|
||||||
|
## Final verification
|
||||||
|
|
||||||
|
```text
|
||||||
|
npm run verify:approved-assets
|
||||||
|
# passed
|
||||||
|
|
||||||
|
npm run test:unit -- tests/integration/public-content.test.ts \
|
||||||
|
tests/unit/ProjectMedia.test.ts tests/unit/content-schemas.test.ts \
|
||||||
|
tests/integration/materialize-empty-asset-set.test.mjs \
|
||||||
|
tests/unit/entry-client.test.ts tests/unit/metadata.test.ts \
|
||||||
|
tests/integration/prerender.test.ts
|
||||||
|
# 7 files passed; 69/69 tests passed
|
||||||
|
|
||||||
|
npm run typecheck
|
||||||
|
# passed
|
||||||
|
|
||||||
|
git diff --check
|
||||||
|
# passed
|
||||||
|
|
||||||
|
npm run build:preview
|
||||||
|
# verify:approved-assets passed
|
||||||
|
# typecheck passed
|
||||||
|
# 49 files passed; 652/652 unit/integration tests passed
|
||||||
|
# client and SSR builds passed
|
||||||
|
# preview prerender and artifact verification passed
|
||||||
|
# transfer budgets passed with no violations
|
||||||
|
|
||||||
|
npx playwright test tests/browser/media-policy.spec.ts \
|
||||||
|
tests/browser/media-failure.spec.ts tests/browser/responsive-layout.spec.ts \
|
||||||
|
tests/browser/accessibility-reflow.spec.ts tests/browser/visual-regression.spec.ts \
|
||||||
|
--project=desktop-chromium --project=mobile-chromium --workers=2
|
||||||
|
# 64 passed; 22 intentionally project/mode-skipped; 0 failed
|
||||||
|
```
|
||||||
|
|
||||||
|
The Playwright commands required normal out-of-sandbox browser-launch approval in this environment; the in-scope commands were rerun through that approved path.
|
||||||
|
|
||||||
|
## Commit scope
|
||||||
|
|
||||||
|
Commit message: `content: integrate approved project evidence`
|
||||||
|
|
||||||
|
The commit contains only the 14 approved implementation/test/checklist files plus this report:
|
||||||
|
|
||||||
|
```text
|
||||||
|
content/public/portfolio-content.json
|
||||||
|
docs/content-approval-checklist.md
|
||||||
|
src/styles/components.css
|
||||||
|
src/styles/print.css
|
||||||
|
tests/browser/accessibility-reflow.spec.ts
|
||||||
|
tests/browser/media-failure.spec.ts
|
||||||
|
tests/browser/media-policy.spec.ts
|
||||||
|
tests/browser/responsive-layout.spec.ts
|
||||||
|
tests/browser/visual-regression.spec.ts
|
||||||
|
tests/integration/prerender.test.ts
|
||||||
|
tests/integration/public-content.test.ts
|
||||||
|
tests/unit/ProjectMedia.test.ts
|
||||||
|
tests/unit/entry-client.test.ts
|
||||||
|
tests/unit/metadata.test.ts
|
||||||
|
.superpowers/sdd/2026-09-04-portfolio-content-seo-media-implementation/task-6-report.md
|
||||||
|
```
|
||||||
|
|
||||||
|
No generated build output, private artifact, temporary inspection harness, screenshot, or unrelated file is staged.
|
||||||
@@ -128,7 +128,35 @@
|
|||||||
"Support schedules, as-needed entries, stock tracking, dose history, reminders, background sync, exports, analytics, an Android widget, and English/Romanian localization."
|
"Support schedules, as-needed entries, stock tracking, dose history, reminders, background sync, exports, analytics, an Android widget, and English/Romanian localization."
|
||||||
],
|
],
|
||||||
"outcome": "The system has supported Luca's private medication tracking consistently for several months. Real use exposed Android reminder failures; investigation restored dependable delivery, and current work is replacing the remaining recurring behavior with occurrence-specific reconciliation to address duplicate and stale reminders. The project works for its current private use but is not presented as production-ready.",
|
"outcome": "The system has supported Luca's private medication tracking consistently for several months. Real use exposed Android reminder failures; investigation restored dependable delivery, and current work is replacing the remaining recurring behavior with occurrence-specific reconciliation to address duplicate and stale reminders. The project works for its current private use but is not presented as production-ready.",
|
||||||
"assetIds": []
|
"assetIds": [
|
||||||
|
"medication-mobile-logs",
|
||||||
|
"medication-mobile-data-settings",
|
||||||
|
"medication-mobile-notification-settings"
|
||||||
|
],
|
||||||
|
"mediaLayout": "portrait-strip",
|
||||||
|
"media": [
|
||||||
|
{
|
||||||
|
"kind": "image",
|
||||||
|
"assetId": "medication-mobile-logs",
|
||||||
|
"alt": "Medication activity screen using an unmistakably fictional demo profile.",
|
||||||
|
"caption": "Daily activity and history in a fictional demo state.",
|
||||||
|
"longDescription": "The mobile view groups scheduled entries and their recorded status without exposing real medication information."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "image",
|
||||||
|
"assetId": "medication-mobile-data-settings",
|
||||||
|
"alt": "Data and synchronization settings for a fictional medication-tracker profile.",
|
||||||
|
"caption": "Data controls supporting the offline-first workflow.",
|
||||||
|
"longDescription": "The settings view exposes synchronization and local-data controls used by the private self-hosted system."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "image",
|
||||||
|
"assetId": "medication-mobile-notification-settings",
|
||||||
|
"alt": "Reminder settings for fictional medication entries.",
|
||||||
|
"caption": "Reminder controls refined through months of private use.",
|
||||||
|
"longDescription": "The settings view groups notification behavior and the Android alarm capability used for scheduled reminders."
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "footy-tracker",
|
"id": "footy-tracker",
|
||||||
@@ -163,7 +191,40 @@
|
|||||||
],
|
],
|
||||||
"outcome": "The project produced useful exploratory visualizations and made the model's assumptions and limitations visible; conclusions remain limited to the simulated scenarios.",
|
"outcome": "The project produced useful exploratory visualizations and made the model's assumptions and limitations visible; conclusions remain limited to the simulated scenarios.",
|
||||||
"url": "https://github.com/LucaPisl/opinion-dynamics-simulation",
|
"url": "https://github.com/LucaPisl/opinion-dynamics-simulation",
|
||||||
"assetIds": []
|
"assetIds": [
|
||||||
|
"opinion-polarization-chart",
|
||||||
|
"opinion-final-distribution-chart"
|
||||||
|
],
|
||||||
|
"mediaLayout": "chart-pair",
|
||||||
|
"media": [
|
||||||
|
{
|
||||||
|
"kind": "chart",
|
||||||
|
"assetId": "opinion-polarization-chart",
|
||||||
|
"alt": "Line chart showing the modeled polarization index increasing across simulation steps.",
|
||||||
|
"caption": "Polarization rose quickly and then increased more gradually in this modeled run.",
|
||||||
|
"chart": {
|
||||||
|
"variables": "Horizontal axis: simulation step. Vertical axis: polarization index.",
|
||||||
|
"trend": "The index rises sharply during the first two steps, then continues upward more slowly.",
|
||||||
|
"conclusion": "This modeled scenario ended more polarized than it began.",
|
||||||
|
"table": {
|
||||||
|
"caption": "Selected modeled polarization values",
|
||||||
|
"columns": ["Step", "Polarization index"],
|
||||||
|
"rows": [["0", "0.765"], ["14", "0.932"]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "chart",
|
||||||
|
"assetId": "opinion-final-distribution-chart",
|
||||||
|
"alt": "Histogram showing one large cluster and several smaller clusters in the final modeled opinions.",
|
||||||
|
"caption": "The final modeled opinions form one dominant cluster and several smaller groups.",
|
||||||
|
"chart": {
|
||||||
|
"variables": "Horizontal axis: final modeled opinion. Vertical axis: frequency.",
|
||||||
|
"trend": "Most modeled opinions cluster near one end of the range, with several smaller groups separated from it.",
|
||||||
|
"conclusion": "This simulated scenario ended with an uneven distribution rather than a single uniform group."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"additional": [
|
"additional": [
|
||||||
@@ -206,7 +267,17 @@
|
|||||||
"technicalDecisions": ["Uses .NET and Avalonia to explore a typed desktop interface."],
|
"technicalDecisions": ["Uses .NET and Avalonia to explore a typed desktop interface."],
|
||||||
"outcome": "A scoped project for developing desktop application fundamentals.",
|
"outcome": "A scoped project for developing desktop application fundamentals.",
|
||||||
"url": "https://github.com/LucaPisl/CurrencyConverterAvalonia",
|
"url": "https://github.com/LucaPisl/CurrencyConverterAvalonia",
|
||||||
"assetIds": []
|
"assetIds": ["currency-converter-interface"],
|
||||||
|
"mediaLayout": "single",
|
||||||
|
"media": [
|
||||||
|
{
|
||||||
|
"kind": "image",
|
||||||
|
"assetId": "currency-converter-interface",
|
||||||
|
"alt": "Currency conversion interface with source and destination currency lists, a conversion date, and calculated results.",
|
||||||
|
"caption": "A focused Avalonia desktop workflow for selecting currencies and reviewing conversion results.",
|
||||||
|
"longDescription": "The interface lets a user choose one source currency, select multiple destination currencies, set a conversion date, and review the resulting values."
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "linux-modding-guide",
|
"id": "linux-modding-guide",
|
||||||
@@ -290,7 +361,8 @@
|
|||||||
},
|
},
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"title": "Luca Pîslaru — System Administrator & Full-Stack Developer",
|
"title": "Luca Pîslaru — System Administrator & Full-Stack Developer",
|
||||||
"description": "Portfolio of Luca Pîslaru, a Bucharest-based System Administrator and Full-Stack Developer working across Linux infrastructure and end-to-end software delivery."
|
"description": "Portfolio of Luca Pîslaru, a Bucharest-based System Administrator and Full-Stack Developer working across Linux infrastructure and end-to-end software delivery.",
|
||||||
|
"socialPreviewAssetId": "social-preview-nightdrive"
|
||||||
},
|
},
|
||||||
"soundtrack": {
|
"soundtrack": {
|
||||||
"intro": {
|
"intro": {
|
||||||
|
|||||||
@@ -47,7 +47,14 @@ This checklist records the Phase 1 public projection. Every item is either inclu
|
|||||||
| Contact | Current or role-specific CV by direct contact | approved |
|
| Contact | Current or role-specific CV by direct contact | approved |
|
||||||
| Metadata | Page title | approved |
|
| Metadata | Page title | approved |
|
||||||
| Metadata | Page description | approved |
|
| Metadata | Page description | approved |
|
||||||
| Metadata | Social preview asset | omit |
|
| Metadata | Social preview asset `social-preview-nightdrive` | approved |
|
||||||
| Assets | Public asset records | omit |
|
| Project media | `medication-mobile-logs` | approved |
|
||||||
| Assets | Project and social media | pending approval |
|
| Project media | `medication-mobile-data-settings` | approved |
|
||||||
| Assets | Toolkit and metadata asset references | omit |
|
| Project media | `medication-mobile-notification-settings` | approved |
|
||||||
|
| Project media | `opinion-polarization-chart` | approved |
|
||||||
|
| Project media | `opinion-final-distribution-chart` | approved |
|
||||||
|
| Project media | `currency-converter-interface` | approved |
|
||||||
|
| Project media | Footy Tracker broadcast frame | deliberately omitted — display rights unconfirmed |
|
||||||
|
| Assets | Exact public project and social asset records | approved |
|
||||||
|
| Assets | Unapproved or rights-unconfirmed media | omit |
|
||||||
|
| Assets | Toolkit asset references | omit |
|
||||||
|
|||||||
@@ -747,6 +747,16 @@
|
|||||||
.project-media-gallery[data-media-layout="chart-pair"] > .project-media-gallery__item > .project-media > :not(:is(img, video, figcaption)) {
|
.project-media-gallery[data-media-layout="chart-pair"] > .project-media-gallery__item > .project-media > :not(:is(img, video, figcaption)) {
|
||||||
grid-row: 3;
|
grid-row: 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.project-media-gallery[data-media-layout="chart-pair"] > .project-media-gallery__item > .project-media > .chart-description {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-media-gallery[data-media-layout="chart-pair"] > .project-media-gallery__item > .project-media > .chart-values {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 4;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-media img,
|
.project-media img,
|
||||||
|
|||||||
@@ -112,6 +112,9 @@
|
|||||||
break-inside: avoid;
|
break-inside: avoid;
|
||||||
margin: 0 0 0.9rem;
|
margin: 0 0 0.9rem;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
|
border-color: var(--line);
|
||||||
|
background: #fff;
|
||||||
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-media-gallery img,
|
.project-media-gallery img,
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ const stylesheetFiles = [
|
|||||||
"src/styles/world.css",
|
"src/styles/world.css",
|
||||||
"src/styles/print.css",
|
"src/styles/print.css",
|
||||||
];
|
];
|
||||||
|
const approvedMedicationAssetIds = new Set([
|
||||||
|
"medication-mobile-logs",
|
||||||
|
"medication-mobile-data-settings",
|
||||||
|
"medication-mobile-notification-settings",
|
||||||
|
]);
|
||||||
|
|
||||||
type ServerRenderer = { render: (url: string, inputs: unknown) => Promise<string> };
|
type ServerRenderer = { render: (url: string, inputs: unknown) => Promise<string> };
|
||||||
type InputValidator = { validateProductionInputs: (content: unknown, manifest: unknown, site: unknown, preparedAssetRoot: URL) => unknown };
|
type InputValidator = { validateProductionInputs: (content: unknown, manifest: unknown, site: unknown, preparedAssetRoot: URL) => unknown };
|
||||||
@@ -49,6 +54,8 @@ function withSyntheticProjectMedia(contentInput: unknown, manifestInput: unknown
|
|||||||
] as const;
|
] as const;
|
||||||
const project = content.projects.selected[0]!;
|
const project = content.projects.selected[0]!;
|
||||||
|
|
||||||
|
manifest.assets = manifest.assets.filter((asset) =>
|
||||||
|
typeof asset.id !== "string" || !approvedMedicationAssetIds.has(asset.id));
|
||||||
project.assetIds = media.map((presentation) => presentation.assetId);
|
project.assetIds = media.map((presentation) => presentation.assetId);
|
||||||
project.media = media;
|
project.media = media;
|
||||||
project.mediaLayout = "portrait-strip";
|
project.mediaLayout = "portrait-strip";
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ test('F8 remains authenticated and request-free while the public manifest has no
|
|||||||
await page.goto(authorizedFaultUrl(distUrl, 'F8-video-failure'), { waitUntil: 'networkidle' });
|
await page.goto(authorizedFaultUrl(distUrl, 'F8-video-failure'), { waitUntil: 'networkidle' });
|
||||||
|
|
||||||
expect((await readFaultSnapshot(page)).fault).toBe('F8-video-failure');
|
expect((await readFaultSnapshot(page)).fault).toBe('F8-video-failure');
|
||||||
await expect(page.locator('[data-project-media]')).toHaveCount(0);
|
|
||||||
await expect(page.locator('video, video source')).toHaveCount(0);
|
await expect(page.locator('video, video source')).toHaveCount(0);
|
||||||
expect(mediaRequests).toEqual([]);
|
expect(mediaRequests).toEqual([]);
|
||||||
await assertSemanticFaultOracle(page);
|
await assertSemanticFaultOracle(page);
|
||||||
|
|||||||
@@ -1,28 +1,150 @@
|
|||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { resolve } from 'node:path';
|
||||||
import { expect, test } from '../helpers/serve-dist';
|
import { expect, test } from '../helpers/serve-dist';
|
||||||
|
import { parsePortfolioContent, parsePublicAssetManifest } from '../../src/content/schemas';
|
||||||
|
|
||||||
test('pending approval renders a complete text-first portfolio without media requests or broken placeholders', async ({ page, distUrl }) => {
|
const fixture = (path: string): unknown =>
|
||||||
const mediaRequests: string[] = [];
|
JSON.parse(readFileSync(resolve(process.cwd(), path), 'utf8')) as unknown;
|
||||||
|
const content = parsePortfolioContent(fixture('content/public/portfolio-content.json'));
|
||||||
|
const manifest = parsePublicAssetManifest(fixture('content/public/asset-manifest.json'));
|
||||||
|
const projectPresentations = [...content.projects.selected, ...content.projects.additional]
|
||||||
|
.flatMap((project) => project.media ?? []);
|
||||||
|
const approvedStillCount = projectPresentations.filter(({ kind }) => kind !== 'video').length;
|
||||||
|
const expectedImages = projectPresentations.flatMap((presentation) => {
|
||||||
|
if (presentation.kind === 'video') return [];
|
||||||
|
const asset = manifest.assets.find(({ id }) => id === presentation.assetId)!;
|
||||||
|
return [{
|
||||||
|
src: `/assets/${asset.fileName}`,
|
||||||
|
width: String(asset.width),
|
||||||
|
height: String(asset.height),
|
||||||
|
alt: presentation.alt,
|
||||||
|
}];
|
||||||
|
});
|
||||||
|
|
||||||
|
test('approved still evidence uses manifest assets and intrinsic lazy image semantics', async ({ page, distUrl }) => {
|
||||||
|
const videoRequests: string[] = [];
|
||||||
page.on('request', (request) => {
|
page.on('request', (request) => {
|
||||||
if (/\.(?:avif|jpe?g|png|webp|mp4|webm)(?:$|\?)/iu.test(request.url())) mediaRequests.push(request.url());
|
if (/\.(?:mp4|webm)(?:$|[?#])/iu.test(request.url())) videoRequests.push(request.url());
|
||||||
});
|
});
|
||||||
await page.goto(distUrl, { waitUntil: 'networkidle' });
|
await page.goto(distUrl, { waitUntil: 'networkidle' });
|
||||||
|
|
||||||
await expect(page.locator('main > section')).toHaveCount(8);
|
await expect(page.locator('main > section')).toHaveCount(8);
|
||||||
await expect(page.locator('#selected-builds article')).toHaveCount(3);
|
await expect(page.locator('#selected-builds article')).toHaveCount(3);
|
||||||
await expect(page.locator('#additional-work article')).toHaveCount(4);
|
await expect(page.locator('#additional-work article')).toHaveCount(4);
|
||||||
await expect(page.locator('[data-project-media]')).toHaveCount(0);
|
await expect(page.locator('[data-project-media]')).toHaveCount(approvedStillCount);
|
||||||
|
await expect(page.locator('[data-project-media] img')).toHaveCount(expectedImages.length);
|
||||||
|
expect(await page.locator('[data-project-media] img').evaluateAll((images) => images.map((image) => ({
|
||||||
|
src: new URL((image as HTMLImageElement).src).pathname,
|
||||||
|
width: image.getAttribute('width'),
|
||||||
|
height: image.getAttribute('height'),
|
||||||
|
alt: image.getAttribute('alt'),
|
||||||
|
loading: (image as HTMLImageElement).loading,
|
||||||
|
decoding: (image as HTMLImageElement).decoding,
|
||||||
|
})))).toEqual(expectedImages.map((image) => ({ ...image, loading: 'lazy', decoding: 'async' })));
|
||||||
|
await expect(page.locator('video, video source')).toHaveCount(0);
|
||||||
|
await expect(page.locator('[data-project-id="private-medication-tracker"] a')).toHaveCount(0);
|
||||||
await expect(page.locator('.journey-poster')).not.toHaveAttribute('src', /.+/u);
|
await expect(page.locator('.journey-poster')).not.toHaveAttribute('src', /.+/u);
|
||||||
expect(mediaRequests).toEqual([]);
|
expect(videoRequests).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('reduced motion does not introduce automatic project media payloads', async ({ page, distUrl }) => {
|
test('Static and reduced motion keep approved stills without introducing video behavior', async ({ page, distUrl }) => {
|
||||||
const videoRequests: string[] = [];
|
const videoRequests: string[] = [];
|
||||||
page.on('request', (request) => {
|
page.on('request', (request) => {
|
||||||
if (/\.(?:mp4|webm)(?:$|\?)/iu.test(request.url())) videoRequests.push(request.url());
|
if (/\.(?:mp4|webm)(?:$|[?#])/iu.test(request.url())) videoRequests.push(request.url());
|
||||||
});
|
});
|
||||||
await page.emulateMedia({ reducedMotion: 'reduce' });
|
await page.addInitScript(() => localStorage.setItem('portfolio.motion.maximum-quality', 'static'));
|
||||||
await page.goto(distUrl, { waitUntil: 'networkidle' });
|
await page.goto(distUrl, { waitUntil: 'networkidle' });
|
||||||
|
|
||||||
|
await expect(page.locator('[data-project-media] img')).toHaveCount(approvedStillCount);
|
||||||
|
await expect(page.locator('video[src], video source[src]')).toHaveCount(0);
|
||||||
|
|
||||||
|
await page.emulateMedia({ reducedMotion: 'reduce' });
|
||||||
|
await page.reload({ waitUntil: 'networkidle' });
|
||||||
|
|
||||||
|
await expect(page.locator('[data-project-media] img')).toHaveCount(approvedStillCount);
|
||||||
await expect(page.locator('video[src], video source[src]')).toHaveCount(0);
|
await expect(page.locator('video[src], video source[src]')).toHaveCount(0);
|
||||||
expect(videoRequests).toEqual([]);
|
expect(videoRequests).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('failed project images leave titles, captions, chart explanations, and alt text in the document', async ({ page, distUrl }) => {
|
||||||
|
await page.route('**/assets/project-*.webp', (route) => route.abort('failed'));
|
||||||
|
await page.goto(distUrl, { waitUntil: 'networkidle' });
|
||||||
|
|
||||||
|
await expect(page.locator('[data-project-media] img')).toHaveCount(approvedStillCount);
|
||||||
|
await expect(page.locator('[data-project-id="private-medication-tracker"]')).toContainText('Private self-hosted medication tracker');
|
||||||
|
await expect(page.locator('[data-project-id="private-medication-tracker"] figcaption')).toHaveCount(3);
|
||||||
|
await expect(page.locator('[data-project-id="opinion-dynamics-simulation"] [data-testid="chart-conclusion"]')).toHaveCount(2);
|
||||||
|
await expect(page.locator('[data-project-id="currency-converter"] img')).toHaveAttribute('alt', /conversion interface/iu);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('desktop chart descriptions and exact values occupy sequential full-width rows', async ({ page, distUrl }, testInfo) => {
|
||||||
|
test.skip(testInfo.project.name !== 'desktop-chromium', 'Desktop chart grid geometry is verified once.');
|
||||||
|
await page.setViewportSize({ width: 1440, height: 900 });
|
||||||
|
await page.addInitScript(() => localStorage.setItem('portfolio.motion.maximum-quality', 'static'));
|
||||||
|
await page.goto(distUrl, { waitUntil: 'networkidle' });
|
||||||
|
|
||||||
|
const chart = page.locator('[data-project-id="opinion-dynamics-simulation"] .project-media--chart').first();
|
||||||
|
const geometry = await chart.evaluate((figure) => {
|
||||||
|
const description = figure.querySelector<HTMLElement>('.chart-description')!;
|
||||||
|
const table = figure.querySelector<HTMLElement>('.chart-values')!;
|
||||||
|
const figureRect = figure.getBoundingClientRect();
|
||||||
|
const descriptionStyle = getComputedStyle(description);
|
||||||
|
const tableStyle = getComputedStyle(table);
|
||||||
|
return {
|
||||||
|
descriptionColumn: descriptionStyle.gridColumnStart,
|
||||||
|
descriptionRow: descriptionStyle.gridRowStart,
|
||||||
|
tableColumn: tableStyle.gridColumnStart,
|
||||||
|
tableRow: tableStyle.gridRowStart,
|
||||||
|
descriptionWidthRatio: description.getBoundingClientRect().width / figureRect.width,
|
||||||
|
tableWidthRatio: table.getBoundingClientRect().width / figureRect.width,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const opinionHeight = await page.locator('[data-project-id="opinion-dynamics-simulation"]')
|
||||||
|
.evaluate((article) => article.getBoundingClientRect().height);
|
||||||
|
|
||||||
|
expect(geometry).toEqual({
|
||||||
|
descriptionColumn: '1',
|
||||||
|
descriptionRow: '3',
|
||||||
|
tableColumn: '1',
|
||||||
|
tableRow: '4',
|
||||||
|
descriptionWidthRatio: expect.any(Number),
|
||||||
|
tableWidthRatio: expect.any(Number),
|
||||||
|
});
|
||||||
|
expect(geometry.descriptionWidthRatio).toBeGreaterThanOrEqual(0.8);
|
||||||
|
expect(geometry.tableWidthRatio).toBeGreaterThanOrEqual(0.8);
|
||||||
|
expect(opinionHeight).toBeLessThan(3_000);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('print project captions and evidence text retain readable contrast', async ({ page, distUrl }, testInfo) => {
|
||||||
|
test.skip(testInfo.project.name !== 'desktop-chromium', 'Print contrast is verified once.');
|
||||||
|
await page.setViewportSize({ width: 1440, height: 900 });
|
||||||
|
await page.emulateMedia({ media: 'print' });
|
||||||
|
await page.goto(distUrl, { waitUntil: 'networkidle' });
|
||||||
|
|
||||||
|
const result = await page.locator('.project-media').evaluateAll((figures) => {
|
||||||
|
const channel = (value: number): number => {
|
||||||
|
const normalized = value / 255;
|
||||||
|
return normalized <= 0.04045 ? normalized / 12.92 : ((normalized + 0.055) / 1.055) ** 2.4;
|
||||||
|
};
|
||||||
|
const luminance = (color: string): number => {
|
||||||
|
const values = color.match(/[\d.]+/gu)?.slice(0, 3).map(Number) ?? [0, 0, 0];
|
||||||
|
return 0.2126 * channel(values[0]!) + 0.7152 * channel(values[1]!) + 0.0722 * channel(values[2]!);
|
||||||
|
};
|
||||||
|
const contrast = (foreground: string, background: string): number => {
|
||||||
|
const [lighter, darker] = [luminance(foreground), luminance(background)].sort((a, b) => b - a);
|
||||||
|
return (lighter! + 0.05) / (darker! + 0.05);
|
||||||
|
};
|
||||||
|
const samples = figures.flatMap((figure) => {
|
||||||
|
const background = getComputedStyle(figure).backgroundColor;
|
||||||
|
return [...figure.querySelectorAll<HTMLElement>('figcaption, .media-description, .chart-description, .chart-values')]
|
||||||
|
.map((element) => contrast(getComputedStyle(element).color, background));
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
backgrounds: [...new Set(figures.map((figure) => getComputedStyle(figure).backgroundColor))],
|
||||||
|
minimumContrast: Math.min(...samples),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.backgrounds).toEqual(['rgb(255, 255, 255)']);
|
||||||
|
expect(result.minimumContrast).toBeGreaterThanOrEqual(4.5);
|
||||||
|
});
|
||||||
|
|||||||
@@ -37,6 +37,11 @@ const syntheticChartPair = [
|
|||||||
{ kind: 'image', assetId: 'synthetic-chart-wide', alt: 'Synthetic wide chart evidence.', caption: 'Fictional wide chart.' },
|
{ kind: 'image', assetId: 'synthetic-chart-wide', alt: 'Synthetic wide chart evidence.', caption: 'Fictional wide chart.' },
|
||||||
{ kind: 'image', assetId: 'synthetic-chart-tall', alt: 'Synthetic tall chart evidence.', caption: 'Fictional tall chart.' },
|
{ kind: 'image', assetId: 'synthetic-chart-tall', alt: 'Synthetic tall chart evidence.', caption: 'Fictional tall chart.' },
|
||||||
] as const;
|
] as const;
|
||||||
|
const approvedMedicationAssetIds = new Set([
|
||||||
|
'medication-mobile-logs',
|
||||||
|
'medication-mobile-data-settings',
|
||||||
|
'medication-mobile-notification-settings',
|
||||||
|
]);
|
||||||
|
|
||||||
function withSyntheticProjectMedia(contentInput: unknown, manifestInput: unknown): { content: unknown; manifest: unknown } {
|
function withSyntheticProjectMedia(contentInput: unknown, manifestInput: unknown): { content: unknown; manifest: unknown } {
|
||||||
const content = structuredClone(contentInput) as {
|
const content = structuredClone(contentInput) as {
|
||||||
@@ -45,6 +50,8 @@ function withSyntheticProjectMedia(contentInput: unknown, manifestInput: unknown
|
|||||||
const manifest = structuredClone(manifestInput) as { assets: Array<Record<string, unknown>> };
|
const manifest = structuredClone(manifestInput) as { assets: Array<Record<string, unknown>> };
|
||||||
const project = content.projects.selected[0]!;
|
const project = content.projects.selected[0]!;
|
||||||
|
|
||||||
|
manifest.assets = manifest.assets.filter((asset) =>
|
||||||
|
typeof asset.id !== 'string' || !approvedMedicationAssetIds.has(asset.id));
|
||||||
project.assetIds = syntheticProjectMedia.map((media) => media.assetId);
|
project.assetIds = syntheticProjectMedia.map((media) => media.assetId);
|
||||||
project.media = syntheticProjectMedia;
|
project.media = syntheticProjectMedia;
|
||||||
project.mediaLayout = 'portrait-strip';
|
project.mediaLayout = 'portrait-strip';
|
||||||
@@ -221,7 +228,7 @@ test('aligns chart-pair captions for deliberately mixed source aspect ratios', a
|
|||||||
await page.setViewportSize({ width: 1440, height: 900 });
|
await page.setViewportSize({ width: 1440, height: 900 });
|
||||||
await loadInformationLayer(page);
|
await loadInformationLayer(page);
|
||||||
|
|
||||||
const captions = page.locator('.project-media-gallery[data-media-layout="chart-pair"] figcaption');
|
const captions = page.locator('[data-project-id="footy-tracker"] .project-media-gallery[data-media-layout="chart-pair"] figcaption');
|
||||||
await expect(captions).toHaveCount(2);
|
await expect(captions).toHaveCount(2);
|
||||||
const tops = await captions.evaluateAll((nodes) => nodes.map((node) => node.getBoundingClientRect().top));
|
const tops = await captions.evaluateAll((nodes) => nodes.map((node) => node.getBoundingClientRect().top));
|
||||||
expect(Math.abs(tops[0]! - tops[1]!)).toBeLessThanOrEqual(1);
|
expect(Math.abs(tops[0]! - tops[1]!)).toBeLessThanOrEqual(1);
|
||||||
|
|||||||
@@ -1,6 +1,17 @@
|
|||||||
import sharp from "sharp";
|
import sharp from "sharp";
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { resolve } from "node:path";
|
||||||
import type { Locator, Page, TestInfo } from "@playwright/test";
|
import type { Locator, Page, TestInfo } from "@playwright/test";
|
||||||
import { expect, test } from "../helpers/serve-dist";
|
import { expect, test } from "../helpers/serve-dist";
|
||||||
|
import { parsePortfolioContent } from "../../src/content/schemas";
|
||||||
|
|
||||||
|
const content = parsePortfolioContent(JSON.parse(
|
||||||
|
readFileSync(resolve(process.cwd(), "content/public/portfolio-content.json"), "utf8"),
|
||||||
|
) as unknown);
|
||||||
|
const approvedProjectStillCount = [
|
||||||
|
...content.projects.selected,
|
||||||
|
...content.projects.additional,
|
||||||
|
].flatMap((project) => project.media ?? []).filter(({ kind }) => kind !== "video").length;
|
||||||
|
|
||||||
interface VisualViewportCase {
|
interface VisualViewportCase {
|
||||||
readonly label: string;
|
readonly label: string;
|
||||||
@@ -301,7 +312,7 @@ test.describe("production visual composition", () => {
|
|||||||
expect(composition.textShadows.every((shadow) => shadow === "none")).toBe(
|
expect(composition.textShadows.every((shadow) => shadow === "none")).toBe(
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
expect(composition.publicProjectMediaCount).toBe(0);
|
expect(composition.publicProjectMediaCount).toBe(approvedProjectStillCount);
|
||||||
expect(composition.fixedSurfaceOverlaps).toEqual([]);
|
expect(composition.fixedSurfaceOverlaps).toEqual([]);
|
||||||
expect(palette.dark).toBeGreaterThan(0.58);
|
expect(palette.dark).toBeGreaterThan(0.58);
|
||||||
expect(palette.restrainedAccent).toBeGreaterThan(0.0005);
|
expect(palette.restrainedAccent).toBeGreaterThan(0.0005);
|
||||||
|
|||||||
@@ -81,14 +81,17 @@ describe('raw prerendered document', () => {
|
|||||||
expect(readProductionInputsSnapshot(document)).toEqual(inputs);
|
expect(readProductionInputsSnapshot(document)).toEqual(inputs);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('emits one canonical, one safe ProfilePage, and no fabricated preview image', async () => {
|
it('emits one canonical, one safe ProfilePage, and the approved preview image', async () => {
|
||||||
const { html } = await renderRawArtifact();
|
const { html, inputs } = await renderRawArtifact();
|
||||||
const document = new DOMParser().parseFromString(html, 'text/html');
|
const document = new DOMParser().parseFromString(html, 'text/html');
|
||||||
const jsonLdNodes = document.querySelectorAll('script[type="application/ld+json"]');
|
const jsonLdNodes = document.querySelectorAll('script[type="application/ld+json"]');
|
||||||
|
const previewAsset = inputs.manifest.assets.find(({ id }) => id === inputs.content.metadata.socialPreviewAssetId)!;
|
||||||
|
|
||||||
expect(document.querySelectorAll('link[rel="canonical"]')).toHaveLength(1);
|
expect(document.querySelectorAll('link[rel="canonical"]')).toHaveLength(1);
|
||||||
expect(document.querySelector('link[rel="canonical"]')?.getAttribute('href')).toBe('https://portfolio.localhost/');
|
expect(document.querySelector('link[rel="canonical"]')?.getAttribute('href')).toBe('https://portfolio.localhost/');
|
||||||
expect(document.querySelectorAll('meta[property="og:image"]')).toHaveLength(0);
|
expect(document.querySelectorAll('meta[property="og:image"]')).toHaveLength(1);
|
||||||
|
expect(document.querySelector('meta[property="og:image"]')?.getAttribute('content'))
|
||||||
|
.toBe(`https://portfolio.localhost/assets/${previewAsset.fileName}`);
|
||||||
expect(jsonLdNodes).toHaveLength(1);
|
expect(jsonLdNodes).toHaveLength(1);
|
||||||
const jsonLd = JSON.parse(jsonLdNodes[0]!.textContent ?? '');
|
const jsonLd = JSON.parse(jsonLdNodes[0]!.textContent ?? '');
|
||||||
expect(jsonLd['@type']).toBe('ProfilePage');
|
expect(jsonLd['@type']).toBe('ProfilePage');
|
||||||
|
|||||||
@@ -1,12 +1,66 @@
|
|||||||
import { readFile } from 'node:fs/promises';
|
import { readFile } from 'node:fs/promises';
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { validateProductionInputs } from '@/content/schemas';
|
import { parsePortfolioContent, validateProductionInputs } from '@/content/schemas';
|
||||||
|
|
||||||
const repositoryRoot = process.cwd();
|
const repositoryRoot = process.cwd();
|
||||||
const readJson = async (relativePath: string): Promise<unknown> =>
|
const readJson = async (relativePath: string): Promise<unknown> =>
|
||||||
JSON.parse(await readFile(`${repositoryRoot}/${relativePath}`, 'utf8')) as unknown;
|
JSON.parse(await readFile(`${repositoryRoot}/${relativePath}`, 'utf8')) as unknown;
|
||||||
|
|
||||||
describe('approved public portfolio projection', () => {
|
describe('approved public portfolio projection', () => {
|
||||||
|
it('attaches only the approved project evidence in its reviewed presentation order', async () => {
|
||||||
|
const content = parsePortfolioContent(await readJson('content/public/portfolio-content.json'));
|
||||||
|
const medication = content.projects.selected[0]!;
|
||||||
|
const footy = content.projects.selected[1]!;
|
||||||
|
const opinion = content.projects.selected[2]!;
|
||||||
|
const currency = content.projects.additional.find((project) => project.id === 'currency-converter')!;
|
||||||
|
|
||||||
|
expect(medication).toMatchObject({
|
||||||
|
id: 'private-medication-tracker',
|
||||||
|
mediaLayout: 'portrait-strip',
|
||||||
|
assetIds: [
|
||||||
|
'medication-mobile-logs',
|
||||||
|
'medication-mobile-data-settings',
|
||||||
|
'medication-mobile-notification-settings',
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(medication.media).toHaveLength(3);
|
||||||
|
expect(medication.media?.every(({ alt }) => /fictional|demo/iu.test(alt))).toBe(true);
|
||||||
|
expect(medication.url).toBeUndefined();
|
||||||
|
|
||||||
|
expect(footy.assetIds).toEqual([]);
|
||||||
|
expect(footy.media).toBeUndefined();
|
||||||
|
expect(footy.mediaLayout).toBeUndefined();
|
||||||
|
|
||||||
|
expect(opinion).toMatchObject({
|
||||||
|
mediaLayout: 'chart-pair',
|
||||||
|
assetIds: ['opinion-polarization-chart', 'opinion-final-distribution-chart'],
|
||||||
|
});
|
||||||
|
expect(opinion.media).toHaveLength(2);
|
||||||
|
expect(opinion.media?.every(({ kind }) => kind === 'chart')).toBe(true);
|
||||||
|
expect(opinion.media?.[0]).toMatchObject({
|
||||||
|
kind: 'chart',
|
||||||
|
assetId: 'opinion-polarization-chart',
|
||||||
|
chart: {
|
||||||
|
table: {
|
||||||
|
columns: ['Step', 'Polarization index'],
|
||||||
|
rows: [['0', '0.765'], ['14', '0.932']],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(opinion.media?.[1]).toMatchObject({
|
||||||
|
kind: 'chart',
|
||||||
|
assetId: 'opinion-final-distribution-chart',
|
||||||
|
});
|
||||||
|
if (opinion.media?.[1]?.kind === 'chart') expect(opinion.media[1].chart.table).toBeUndefined();
|
||||||
|
|
||||||
|
expect(currency).toMatchObject({
|
||||||
|
mediaLayout: 'single',
|
||||||
|
assetIds: ['currency-converter-interface'],
|
||||||
|
media: [{ kind: 'image', assetId: 'currency-converter-interface' }],
|
||||||
|
});
|
||||||
|
expect(content.metadata.socialPreviewAssetId).toBe('social-preview-nightdrive');
|
||||||
|
});
|
||||||
|
|
||||||
it('contains approved identity and balanced evidence without prohibited surfaces', async () => {
|
it('contains approved identity and balanced evidence without prohibited surfaces', async () => {
|
||||||
const { content, manifest, site } = validateProductionInputs(
|
const { content, manifest, site } = validateProductionInputs(
|
||||||
await readJson('content/public/portfolio-content.json'),
|
await readJson('content/public/portfolio-content.json'),
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import ProjectMediaGallery from '@/components/ProjectMediaGallery.vue';
|
|||||||
import { useLazyMedia } from '@/composables/useLazyMedia';
|
import { useLazyMedia } from '@/composables/useLazyMedia';
|
||||||
import { parsePortfolioContent, parsePublicAssetManifest } from '@/content/schemas';
|
import { parsePortfolioContent, parsePublicAssetManifest } from '@/content/schemas';
|
||||||
import type { MediaPresentation, ProductionInputs, PublicAssetManifest } from '@/content/types';
|
import type { MediaPresentation, ProductionInputs, PublicAssetManifest } from '@/content/types';
|
||||||
|
import approvedContentJson from '@/../content/public/portfolio-content.json';
|
||||||
|
import approvedManifestJson from '@/../content/public/asset-manifest.json';
|
||||||
|
|
||||||
const sha = 'a'.repeat(64);
|
const sha = 'a'.repeat(64);
|
||||||
const manifest: PublicAssetManifest = {
|
const manifest: PublicAssetManifest = {
|
||||||
@@ -226,6 +228,34 @@ describe('ProjectMedia', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('ProjectMediaGallery', () => {
|
describe('ProjectMediaGallery', () => {
|
||||||
|
it('renders every approved project still with intrinsic, lazy, contextual semantics', () => {
|
||||||
|
const content = parsePortfolioContent(structuredClone(approvedContentJson));
|
||||||
|
const approvedManifest = parsePublicAssetManifest(structuredClone(approvedManifestJson));
|
||||||
|
const approvedInputs = { manifest: approvedManifest, content, site: {} } as unknown as ProductionInputs;
|
||||||
|
const projects = [...content.projects.selected, ...content.projects.additional];
|
||||||
|
const mediaProjects = projects.filter((project) => project.media !== undefined);
|
||||||
|
|
||||||
|
expect(mediaProjects.map(({ id }) => id)).toEqual([
|
||||||
|
'private-medication-tracker',
|
||||||
|
'opinion-dynamics-simulation',
|
||||||
|
'currency-converter',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const wrappers = mediaProjects.map((project) => mount(ProjectMediaGallery, {
|
||||||
|
props: { media: project.media ?? [], layout: project.mediaLayout ?? 'single' },
|
||||||
|
global: { provide: { [productionInputsKey as symbol]: approvedInputs } },
|
||||||
|
}));
|
||||||
|
const images = wrappers.flatMap((wrapper) => wrapper.findAll('img'));
|
||||||
|
|
||||||
|
expect(images).toHaveLength(6);
|
||||||
|
expect(images.every((image) => image.attributes('loading') === 'lazy')).toBe(true);
|
||||||
|
expect(images.every((image) => image.attributes('decoding') === 'async')).toBe(true);
|
||||||
|
expect(images.every((image) => Number(image.attributes('width')) > 0 && Number(image.attributes('height')) > 0)).toBe(true);
|
||||||
|
expect(wrappers[0]!.text()).toMatch(/fictional demo state|private self-hosted system/iu);
|
||||||
|
expect(wrappers[1]!.findAll('[data-testid="chart-variables"]')).toHaveLength(2);
|
||||||
|
expect(wrappers[1]!.findAll('table')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
it('keeps three portrait images lazy, contextual, and in their supplied order', () => {
|
it('keeps three portrait images lazy, contextual, and in their supplied order', () => {
|
||||||
const wrapper = mountGallery([
|
const wrapper = mountGallery([
|
||||||
{ kind: 'image', assetId: 'project-image', alt: 'Synthetic profile screen.', caption: 'Fictional demo profile.' },
|
{ kind: 'image', assetId: 'project-image', alt: 'Synthetic profile screen.', caption: 'Fictional demo profile.' },
|
||||||
|
|||||||
@@ -39,7 +39,10 @@ describe('client production-input snapshot', () => {
|
|||||||
|
|
||||||
it('rejects a supplied asset reference absent from the supplied public manifest', () => {
|
it('rejects a supplied asset reference absent from the supplied public manifest', () => {
|
||||||
const invalid = validInputs();
|
const invalid = validInputs();
|
||||||
invalid.content.projects.selected[0]!.assetIds = ['unmanifested-snapshot-asset'];
|
const medication = invalid.content.projects.selected[0]!;
|
||||||
|
medication.assetIds = medication.assetIds.map((assetId) =>
|
||||||
|
assetId === 'medication-mobile-logs' ? 'unmanifested-snapshot-asset' : assetId);
|
||||||
|
medication.media![0]!.assetId = 'unmanifested-snapshot-asset';
|
||||||
|
|
||||||
expect(() => readProductionInputsSnapshot(snapshotDocument(invalid))).toThrow(/unmanifested asset reference/i);
|
expect(() => readProductionInputsSnapshot(snapshotDocument(invalid))).toThrow(/unmanifested asset reference/i);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -39,7 +39,8 @@ describe('safe metadata', () => {
|
|||||||
'Portfolio of Luca Pîslaru, a Bucharest-based System Administrator and Full-Stack Developer working across Linux infrastructure and end-to-end software delivery.',
|
'Portfolio of Luca Pîslaru, a Bucharest-based System Administrator and Full-Stack Developer working across Linux infrastructure and end-to-end software delivery.',
|
||||||
);
|
);
|
||||||
expect(metadata.canonicalUrl).toBe('https://portfolio.example.test/');
|
expect(metadata.canonicalUrl).toBe('https://portfolio.example.test/');
|
||||||
expect(metadata.openGraph.image).toBeUndefined();
|
const previewAsset = manifest.assets.find(({ id }) => id === content.metadata.socialPreviewAssetId)!;
|
||||||
|
expect(metadata.openGraph.image).toBe(`https://portfolio.example.test/assets/${previewAsset.fileName}`);
|
||||||
expect(metadata.jsonLd).toEqual({
|
expect(metadata.jsonLd).toEqual({
|
||||||
'@context': 'https://schema.org',
|
'@context': 'https://schema.org',
|
||||||
'@type': 'ProfilePage',
|
'@type': 'ProfilePage',
|
||||||
|
|||||||
Reference in New Issue
Block a user