@commonpub/layer 0.71.0 → 0.71.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@commonpub/layer",
|
|
3
|
-
"version": "0.71.
|
|
3
|
+
"version": "0.71.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./nuxt.config.ts",
|
|
6
6
|
"files": [
|
|
@@ -57,12 +57,12 @@
|
|
|
57
57
|
"@commonpub/config": "0.21.0",
|
|
58
58
|
"@commonpub/docs": "0.6.3",
|
|
59
59
|
"@commonpub/editor": "0.7.11",
|
|
60
|
-
"@commonpub/protocol": "0.13.0",
|
|
61
60
|
"@commonpub/explainer": "0.7.15",
|
|
62
|
-
"@commonpub/
|
|
61
|
+
"@commonpub/learning": "0.5.2",
|
|
62
|
+
"@commonpub/protocol": "0.13.0",
|
|
63
63
|
"@commonpub/schema": "0.39.0",
|
|
64
|
+
"@commonpub/server": "2.84.1",
|
|
64
65
|
"@commonpub/theme-studio": "0.5.1",
|
|
65
|
-
"@commonpub/learning": "0.5.2",
|
|
66
66
|
"@commonpub/ui": "0.12.2"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
@@ -40,8 +40,11 @@ const currentRoundId = computed<string | null>(() => {
|
|
|
40
40
|
// The artifact judges review THIS round: the nearest `submission` stage (with a
|
|
41
41
|
// template) preceding the current review stage — round 1 reviews the proposal,
|
|
42
42
|
// round 2 the prototype. Null for classic contests (no templates), which keeps
|
|
43
|
-
// the page byte-identical to pre-artifact behaviour.
|
|
43
|
+
// the page byte-identical to pre-artifact behaviour. Flag-gated so disabling
|
|
44
|
+
// contestStageSubmissions hides the (server-stripped) artifact boxes entirely.
|
|
45
|
+
const { features } = useFeatures();
|
|
44
46
|
const artifactStage = computed(() => {
|
|
47
|
+
if (features.value.contestStageSubmissions === false) return null;
|
|
45
48
|
const c = contest.value;
|
|
46
49
|
if (!c || !currentRoundId.value) return null;
|
|
47
50
|
const stages = normalizeStages(c);
|
|
@@ -38,6 +38,9 @@ export default defineEventHandler(async (event): Promise<ContestEntryItem> => {
|
|
|
38
38
|
|
|
39
39
|
if (!shouldRevealScores(contest.judgingVisibility, contest.status, privileged)) {
|
|
40
40
|
entry.score = null;
|
|
41
|
+
// Per-round snapshot scores honour revealScores too (the cohort outcome
|
|
42
|
+
// itself stays public, mirroring the entries listing).
|
|
43
|
+
entry.stageState = entry.stageState.map((s) => ({ ...s, score: null }));
|
|
41
44
|
}
|
|
42
45
|
if (!privileged) {
|
|
43
46
|
delete entry.judgeScores;
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Component tests for the contest entry-detail page (artifact timeline).
|
|
3
|
-
*
|
|
4
|
-
* Locks: the content summary card, the stage-ordered artifact timeline with
|
|
5
|
-
* template-labelled fields, url fields rendered as safe links, orphaned values
|
|
6
|
-
* (template field later removed) still rendering, artifact section hidden when
|
|
7
|
-
* the server stripped artifacts (unprivileged viewer), and an axe scan.
|
|
8
|
-
*
|
|
9
|
-
* Page uses Nuxt auto-imports (useRoute, useLazyFetch, useSeoMeta, useSiteName,
|
|
10
|
-
* plus the auto-imported contestStages utils) — stub them on globalThis.
|
|
11
|
-
*/
|
|
12
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
13
|
-
import { render } from '@testing-library/vue';
|
|
14
|
-
import { defineComponent, h, ref } from 'vue';
|
|
15
|
-
import axe from 'axe-core';
|
|
16
|
-
import EntryDetailPage from '../[entryId].vue';
|
|
17
|
-
import { normalizeStages, currentStageId } from '../../../../../utils/contestStages';
|
|
18
|
-
|
|
19
|
-
const NuxtLink = defineComponent({
|
|
20
|
-
name: 'NuxtLink',
|
|
21
|
-
props: { to: String },
|
|
22
|
-
setup(props, { slots }) {
|
|
23
|
-
return () => h('a', { href: props.to }, slots.default?.());
|
|
24
|
-
},
|
|
25
|
-
});
|
|
26
|
-
const stubs = { NuxtLink };
|
|
27
|
-
|
|
28
|
-
const STAGES = [
|
|
29
|
-
{ id: 'prop', name: 'Proposals', kind: 'submission', submissionTemplate: [
|
|
30
|
-
{ key: 'summary', label: 'Summary', type: 'textarea', required: true },
|
|
31
|
-
] },
|
|
32
|
-
{ id: 'rev1', name: 'Screening', kind: 'review' },
|
|
33
|
-
{ id: 'proto', name: 'Prototype', kind: 'submission', submissionTemplate: [
|
|
34
|
-
{ key: 'repo_url', label: 'Repository URL', type: 'url', required: true },
|
|
35
|
-
] },
|
|
36
|
-
];
|
|
37
|
-
|
|
38
|
-
function makeContest(overrides: Record<string, unknown> = {}) {
|
|
39
|
-
return {
|
|
40
|
-
title: 'Resilient America',
|
|
41
|
-
status: 'active',
|
|
42
|
-
startDate: '2026-04-01T00:00:00.000Z',
|
|
43
|
-
endDate: '2026-08-01T00:00:00.000Z',
|
|
44
|
-
judgingEndDate: null,
|
|
45
|
-
stages: STAGES,
|
|
46
|
-
currentStageId: 'proto',
|
|
47
|
-
...overrides,
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function makeEntry(overrides: Record<string, unknown> = {}) {
|
|
52
|
-
return {
|
|
53
|
-
id: 'e1',
|
|
54
|
-
contestId: 'c1',
|
|
55
|
-
contentId: 'ct1',
|
|
56
|
-
userId: 'u1',
|
|
57
|
-
score: 88,
|
|
58
|
-
rank: 2,
|
|
59
|
-
stageState: [{ stageId: 'rev1', status: 'advanced' }],
|
|
60
|
-
eliminated: false,
|
|
61
|
-
stageSubmissions: [
|
|
62
|
-
// Deliberately out of stage order — the timeline must sort by stage.
|
|
63
|
-
{ stageId: 'proto', fields: { repo_url: 'https://github.com/x/y' }, submittedAt: '2026-07-01T12:00:00.000Z' },
|
|
64
|
-
{ stageId: 'prop', fields: { summary: 'A mesh network.', legacy_field: 'kept' }, submittedAt: '2026-05-01T12:00:00.000Z' },
|
|
65
|
-
],
|
|
66
|
-
submittedAt: '2026-04-20T12:00:00.000Z',
|
|
67
|
-
contentTitle: 'Solar Mesh Node',
|
|
68
|
-
contentSlug: 'solar-mesh-node',
|
|
69
|
-
contentType: 'project',
|
|
70
|
-
contentCoverImageUrl: null,
|
|
71
|
-
authorName: 'Ada Maker',
|
|
72
|
-
authorUsername: 'ada',
|
|
73
|
-
authorAvatarUrl: null,
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
let contestData: Record<string, unknown> | null = makeContest();
|
|
78
|
-
let entryData: Record<string, unknown> | null = makeEntry();
|
|
79
|
-
|
|
80
|
-
Object.assign(globalThis, {
|
|
81
|
-
useRoute: () => ({ params: { slug: 'resilient', entryId: 'e1' } }),
|
|
82
|
-
useLazyFetch: vi.fn((url: string) => ({
|
|
83
|
-
data: ref(String(url).includes('/entries/') ? entryData : contestData),
|
|
84
|
-
error: ref(null),
|
|
85
|
-
})),
|
|
86
|
-
useSeoMeta: () => {},
|
|
87
|
-
useSiteName: () => 'Test',
|
|
88
|
-
normalizeStages,
|
|
89
|
-
currentStageId,
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
function mount() {
|
|
93
|
-
return render(EntryDetailPage, { global: { stubs } });
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
beforeEach(() => {
|
|
97
|
-
contestData = makeContest();
|
|
98
|
-
entryData = makeEntry();
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
describe('entry detail page', () => {
|
|
102
|
-
it('shows the content summary with author, status badges, and a project link', () => {
|
|
103
|
-
const { container } = mount();
|
|
104
|
-
expect(container.querySelector('.cpub-ed-title')?.textContent).toBe('Solar Mesh Node');
|
|
105
|
-
expect(container.textContent).toContain('Ada Maker');
|
|
106
|
-
expect(container.textContent).toContain('Advanced');
|
|
107
|
-
expect(container.textContent).toContain('#2');
|
|
108
|
-
expect(container.textContent).toContain('Score 88');
|
|
109
|
-
const projectLink = Array.from(container.querySelectorAll('a')).find((a) => a.textContent?.includes('View the project'));
|
|
110
|
-
expect(projectLink?.getAttribute('href')).toBe('/u/ada/project/solar-mesh-node');
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('renders the artifact timeline in stage order with template labels', () => {
|
|
114
|
-
const { container } = mount();
|
|
115
|
-
const names = Array.from(container.querySelectorAll('.cpub-ed-stagename')).map((n) => n.textContent);
|
|
116
|
-
expect(names).toEqual(['Proposals', 'Prototype']); // stage order, not submit order
|
|
117
|
-
expect(container.textContent).toContain('Summary');
|
|
118
|
-
expect(container.textContent).toContain('A mesh network.');
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it('renders url fields as hardened external links', () => {
|
|
122
|
-
const { container } = mount();
|
|
123
|
-
const link = Array.from(container.querySelectorAll('.cpub-ed-fields a')).find((a) => a.textContent === 'https://github.com/x/y');
|
|
124
|
-
expect(link).toBeTruthy();
|
|
125
|
-
expect(link!.getAttribute('rel')).toContain('noopener');
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it('still renders values whose template field was later removed (never drop data)', () => {
|
|
129
|
-
const { container } = mount();
|
|
130
|
-
expect(container.textContent).toContain('legacy_field');
|
|
131
|
-
expect(container.textContent).toContain('kept');
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it('hides the artifact section entirely when the server stripped artifacts', () => {
|
|
135
|
-
// The route handler `delete`s the key for unprivileged viewers — mirror that.
|
|
136
|
-
const stripped = makeEntry();
|
|
137
|
-
delete (stripped as Record<string, unknown>).stageSubmissions;
|
|
138
|
-
entryData = stripped;
|
|
139
|
-
const { container } = mount();
|
|
140
|
-
expect(container.querySelector('.cpub-ed-stages')).toBeNull();
|
|
141
|
-
// The content card still shows — the page is useful to the public.
|
|
142
|
-
expect(container.querySelector('.cpub-ed-title')?.textContent).toBe('Solar Mesh Node');
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it('passes an axe scan', async () => {
|
|
146
|
-
const { container } = mount();
|
|
147
|
-
const results = await axe.run(container);
|
|
148
|
-
expect(results.violations).toEqual([]);
|
|
149
|
-
});
|
|
150
|
-
});
|