File size: 7,503 Bytes
e8a6c67
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
/**
 * Static demo route /q/sample.
 *
 * This is the prerendered worked example with hard-coded sample data —
 * it must render every design-system piece without an SSE connection or
 * a working LLM backend, so it's also the cheapest probe for design-
 * system regressions.
 */
import { test, expect } from '@playwright/test';

test.describe('/q/sample (prerendered demo)', () => {
  test('renders four-section briefing with claim glyphs + cite anchors', async ({ page }) => {
    await page.goto('/q/sample');

    // Header wordmark + region label
    await expect(page.locator('.riprap-wordmark')).toContainText('riprap');
    await expect(page.locator('h1.brief-h1')).toContainText('Flood-exposure briefing');

    // 4 canonical section heads
    const heads = page.locator('.briefing-section-head .briefing-section-num');
    await expect(heads).toHaveCount(4);
    await expect(heads.nth(0)).toHaveText('01');
    await expect(heads.nth(1)).toHaveText('02');
    await expect(heads.nth(2)).toHaveText('03');
    await expect(heads.nth(3)).toHaveText('04');

    // Tier glyphs in the prose gutter (one per claim)
    const claimGlyphs = page.locator('.claim-glyph svg[role="img"]');
    expect(await claimGlyphs.count()).toBeGreaterThan(5);

    // Inline citations link to drawer entries
    const cites = page.locator('a.inline-cite');
    expect(await cites.count()).toBeGreaterThan(5);
  });

  test('renders citation drawer with all 10 sample sources', async ({ page }) => {
    await page.goto('/q/sample');
    const items = page.locator('.citation-drawer .citation-item');
    await expect(items).toHaveCount(10);
    // Each item carries source label + tier glyph + doc id
    await expect(items.first().locator('.citation-source')).toBeVisible();
    await expect(items.first().locator('.citation-docid')).toBeVisible();
  });

  test('renders trace UI with all run steps', async ({ page }) => {
    await page.goto('/q/sample');
    await expect(page.locator('.trace-ui')).toBeVisible();
    // Trace head meta should show a non-zero total
    await expect(page.locator('.trace-head-meta')).toContainText('s total');
  });

  test('renders evidence grid with all 6 viz formats', async ({ page }) => {
    await page.goto('/q/sample');
    const cards = page.locator('.evidence-card');
    await expect(cards).toHaveCount(8);
    // Each tier is represented at least once
    for (const t of ['empirical', 'modeled', 'proxy', 'synthetic']) {
      expect(await page.locator(`.evidence-card-${t}`).count()).toBeGreaterThan(0);
    }
  });

  test('legend hides layers with zero features', async ({ page }) => {
    await page.goto('/q/sample');
    // /q/sample only ships a synthetic fixture (the others are 0).
    // Legend must show synthetic only — silence-over-confabulation
    // applied to the map (handoff hard rule #3).
    await expect(page.locator('.map-legend')).toBeVisible({ timeout: 10_000 });
    const items = page.locator('.map-legend-item');
    await expect(items).toHaveCount(1);
    await expect(items.first().locator('.map-legend-label'))
      .toContainText(/Synthetic SAR/);
    // Empty layers must not be present.
    await expect(page.locator('.map-legend-item', { hasText: 'Sandy' })).toHaveCount(0);
    await expect(page.locator('.map-legend-item', { hasText: '311' })).toHaveCount(0);
  });

  test('MapLibre map mounts and registers syn-stripe-45 pattern', async ({ page }) => {
    const consoleErrors: string[] = [];
    page.on('console', (msg) => {
      if (msg.type() === 'error') consoleErrors.push(msg.text());
    });
    await page.goto('/q/sample');

    // MapLibre canvas mounts
    const canvas = page.locator('.maplibregl-canvas');
    await expect(canvas).toBeVisible({ timeout: 15_000 });

    // Wait for `window.__riprapMap` to appear (RipMap.svelte sets it
    // on `map.on('load')`), then for the syn-stripe registration
    // promise to settle.
    await page.waitForFunction(
      () => Boolean((window as unknown as { __riprapMap?: unknown }).__riprapMap),
      undefined,
      { timeout: 15_000 }
    );
    // SVG → image decode happens asynchronously; give it a beat.
    await page.waitForFunction(
      () => {
        const m = (window as unknown as { __riprapMap?: { hasImage: (s: string) => boolean } })
          .__riprapMap;
        return Boolean(m && m.hasImage('syn-stripe-45'));
      },
      undefined,
      { timeout: 5_000 }
    );

    const mapState = await page.evaluate<{
      hasStripe: boolean;
      hasStripe2x: boolean;
      hasStripeLow: boolean;
      sources: string[];
      layers: string[];
    } | null>(() => {
      type MlMap = {
        loaded: () => boolean;
        hasImage: (id: string) => boolean;
        getStyle: () => { sources: Record<string, unknown>; layers: Array<{ id: string }> };
      };
      const map = (window as unknown as { __riprapMap?: MlMap }).__riprapMap;
      if (!map) return null;
      const style = map.getStyle();
      return {
        hasStripe: map.hasImage('syn-stripe-45'),
        hasStripe2x: map.hasImage('syn-stripe-45-2x'),
        hasStripeLow: map.hasImage('syn-stripe-45-low'),
        sources: Object.keys(style.sources),
        layers: style.layers.map((l) => l.id)
      };
    });

    expect(mapState, 'map instance should be reachable from the DOM').not.toBeNull();
    if (!mapState) return;

    // The sample route ships a synthetic-prior fixture polygon — verify
    // the syn-prior source has a non-empty FeatureCollection so the
    // syn-stripe-45 fill is actually visible (not just registered).
    const synFeatureCount = await page.evaluate<number>(() => {
      type Src = { _data?: { features?: unknown[] } } | undefined;
      type MlMap = { getSource: (id: string) => Src };
      const map = (window as unknown as { __riprapMap?: MlMap }).__riprapMap;
      const src = map?.getSource('syn-prior');
      const data = src?._data as { features?: unknown[] } | undefined;
      return data?.features?.length ?? 0;
    });
    expect(synFeatureCount,
      'syn-prior source should have at least one feature in the /q/sample fixture')
      .toBeGreaterThan(0);

    // The four tier sources are added by RipMap on style.load
    expect(mapState.sources).toContain('sandy-empirical');
    expect(mapState.sources).toContain('dep-modeled');
    expect(mapState.sources).toContain('syn-prior');
    expect(mapState.sources).toContain('proxy-311');
    expect(mapState.sources).toContain('queried-address');

    // Tier layers are added with the canonical ids from the spec
    expect(mapState.layers).toEqual(expect.arrayContaining([
      'tier-empirical-fill',
      'tier-empirical-line',
      'tier-modeled-fill',
      'tier-modeled-line',
      'tier-synthetic-fill',
      'tier-synthetic-line',
      'tier-proxy-dots',
      'queried-pin'
    ]));

    // v0.4.2 §14: syn-stripe pattern image must be registered. This is
    // the exact regression we're guarding against — synthetic SAR not
    // rendering because `fill-pattern: syn-stripe-45` resolves to a
    // missing image.
    expect(mapState.hasStripe, 'syn-stripe-45 image should be registered').toBe(true);
    expect(mapState.hasStripe2x, 'syn-stripe-45-2x image should be registered').toBe(true);
    expect(mapState.hasStripeLow, 'syn-stripe-45-low image should be registered').toBe(true);

    // No console errors during boot
    expect(consoleErrors.filter((e) => !e.includes('favicon')))
      .toEqual([]);
  });
});