@littlebearapps/platform-admin-sdk 2.1.0 → 2.2.0
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/README.md +2 -5
- package/dist/templates.d.ts +1 -1
- package/dist/templates.js +121 -3
- package/package.json +1 -1
- package/templates/full/dashboard/src/components/notifications/NotificationDropdown.tsx +130 -0
- package/templates/full/dashboard/src/components/notifications/NotificationItem.tsx +264 -0
- package/templates/full/dashboard/src/components/patterns/PatternInfoButton.tsx +60 -0
- package/templates/full/dashboard/src/components/reports/FeatureUsageReport.tsx +339 -0
- package/templates/full/dashboard/src/components/search/SearchResultGroup.tsx +46 -0
- package/templates/full/dashboard/src/components/search/SearchResultItem.tsx +212 -0
- package/templates/full/dashboard/src/pages/api/patterns/[id]/approve.ts +49 -0
- package/templates/full/dashboard/src/pages/api/patterns/[id]/reject.ts +50 -0
- package/templates/full/dashboard/src/pages/api/reports/digests/stats.ts +38 -0
- package/templates/full/dashboard/src/pages/api/reports/digests.ts +39 -0
- package/templates/full/dashboard/src/pages/api/search/reindex/[type].ts +56 -0
- package/templates/full/dashboard/src/pages/api/test-reports/[id].ts +102 -0
- package/templates/full/dashboard/src/pages/feedback.astro +365 -0
- package/templates/full/dashboard/src/pages/kiosk.astro +206 -0
- package/templates/full/dashboard/src/pages/map.astro +561 -0
- package/templates/full/dashboard/src/pages/revenue.astro +72 -0
- package/templates/full/dashboard/src/pages/tests.astro +431 -0
- package/templates/full/scripts/ops/audit-cost-anomaly.ts +430 -0
- package/templates/full/scripts/ops/verify-account-total.ts +256 -0
- package/templates/full/tests/integration/feedback-schema.test.ts +361 -0
- package/templates/full/tests/integration/r2-archive.test.ts +108 -0
- package/templates/shared/.github/workflows/dependabot-automerge.yml +41 -0
- package/templates/shared/.github/workflows/validate-controls.yml +27 -0
- package/templates/shared/dashboard/src/components/Breadcrumbs.astro +101 -0
- package/templates/shared/dashboard/src/components/EmptyState.astro +46 -0
- package/templates/shared/dashboard/src/components/ErrorBoundary.astro +79 -0
- package/templates/shared/dashboard/src/components/LoadingSkeleton.astro +105 -0
- package/templates/shared/dashboard/src/components/PageShell.astro +72 -0
- package/templates/shared/dashboard/src/components/SkipLinks.astro +22 -0
- package/templates/shared/dashboard/src/components/Toast.astro +170 -0
- package/templates/shared/dashboard/src/components/ToastContainer.astro +156 -0
- package/templates/shared/dashboard/src/components/costs/ProviderCostsGrid.tsx +401 -0
- package/templates/shared/dashboard/src/components/costs/index.ts +4 -0
- package/templates/shared/dashboard/src/components/overview/AlertBanner.tsx +94 -0
- package/templates/shared/dashboard/src/components/overview/index.ts +9 -0
- package/templates/shared/dashboard/src/components/resources/CostChart.tsx +170 -0
- package/templates/shared/dashboard/src/components/resources/ProviderCard.tsx +272 -0
- package/templates/shared/dashboard/src/components/resources/ProviderDetail.tsx +293 -0
- package/templates/shared/dashboard/src/components/settings/SettingsCard.astro +102 -0
- package/templates/shared/dashboard/src/components/usage/AllowanceGauge.astro +170 -0
- package/templates/shared/dashboard/src/components/usage/AnomalyAlerts.astro +633 -0
- package/templates/shared/dashboard/src/components/usage/BillingCycleCountdown.astro +192 -0
- package/templates/shared/dashboard/src/components/usage/BurnRateHero.astro +539 -0
- package/templates/shared/dashboard/src/components/usage/CircuitBreakerEventLog.astro +542 -0
- package/templates/shared/dashboard/src/components/usage/CircuitBreakerPanel.tsx +292 -0
- package/templates/shared/dashboard/src/components/usage/CircuitBreakerStatus.astro +669 -0
- package/templates/shared/dashboard/src/components/usage/CompactThresholdBanner.astro +531 -0
- package/templates/shared/dashboard/src/components/usage/ComparisonModeSelector.astro +651 -0
- package/templates/shared/dashboard/src/components/usage/CostBreakdownChart.astro +381 -0
- package/templates/shared/dashboard/src/components/usage/CostBreakdownTable.astro +210 -0
- package/templates/shared/dashboard/src/components/usage/CostDataTable.astro +0 -0
- package/templates/shared/dashboard/src/components/usage/CostDonutChart.astro +311 -0
- package/templates/shared/dashboard/src/components/usage/DailyCostChart.astro +632 -0
- package/templates/shared/dashboard/src/components/usage/ExportButton.astro +114 -0
- package/templates/shared/dashboard/src/components/usage/FeatureBudgetsTable.astro +872 -0
- package/templates/shared/dashboard/src/components/usage/FilterBar.astro +190 -0
- package/templates/shared/dashboard/src/components/usage/FilterToggles.astro +175 -0
- package/templates/shared/dashboard/src/components/usage/GitHubUsageCard.astro +537 -0
- package/templates/shared/dashboard/src/components/usage/OverageCostCard.astro +212 -0
- package/templates/shared/dashboard/src/components/usage/PlanUtilizationCard.astro +193 -0
- package/templates/shared/dashboard/src/components/usage/ProjectCard.astro +640 -0
- package/templates/shared/dashboard/src/components/usage/ProjectCardsGrid.astro +272 -0
- package/templates/shared/dashboard/src/components/usage/ResourceSearch.astro +279 -0
- package/templates/shared/dashboard/src/components/usage/ServiceUtilizationList.astro +604 -0
- package/templates/shared/dashboard/src/components/usage/SparklineCard.astro +399 -0
- package/templates/shared/dashboard/src/components/usage/StatsHero.astro +600 -0
- package/templates/shared/dashboard/src/components/usage/TableFilters.astro +1033 -0
- package/templates/shared/dashboard/src/components/usage/ThresholdAlert.astro +271 -0
- package/templates/shared/dashboard/src/components/usage/ThresholdSettings.astro +618 -0
- package/templates/shared/dashboard/src/components/usage/TopSpenderCard.astro +170 -0
- package/templates/shared/dashboard/src/components/usage/UnifiedResourceTable.astro +1737 -0
- package/templates/shared/dashboard/src/components/usage/UsageCard.astro +135 -0
- package/templates/shared/dashboard/src/components/usage/UsageHealthBanner.astro +387 -0
- package/templates/shared/dashboard/src/components/usage/UtilizationBar.astro +159 -0
- package/templates/shared/dashboard/src/components/usage/WorkersBreakdownTable.astro +659 -0
- package/templates/shared/dashboard/src/components/usage/daily/CostChart.astro +461 -0
- package/templates/shared/dashboard/src/components/usage/daily/CostTable.astro +946 -0
- package/templates/shared/dashboard/src/components/usage/daily/DailyOverview.astro +1079 -0
- package/templates/shared/dashboard/src/components/usage/design-tokens.ts +187 -0
- package/templates/shared/dashboard/src/components/usage/filters/InlineDateRange.astro +285 -0
- package/templates/shared/dashboard/src/components/usage/filters/PeriodButtons.astro +157 -0
- package/templates/shared/dashboard/src/components/usage/filters/ProjectSelect.astro +284 -0
- package/templates/shared/dashboard/src/components/usage/scripts/ai-tab-controller.ts +419 -0
- package/templates/shared/dashboard/src/components/usage/scripts/constants.ts +60 -0
- package/templates/shared/dashboard/src/components/usage/scripts/formatters.ts +62 -0
- package/templates/shared/dashboard/src/components/usage/scripts/overview-controller.ts +1633 -0
- package/templates/shared/dashboard/src/components/usage/scripts/resource-table-builder.ts +294 -0
- package/templates/shared/dashboard/src/components/usage/scripts/tabs-filters-controller.ts +464 -0
- package/templates/shared/dashboard/src/components/usage/state/index.ts +55 -0
- package/templates/shared/dashboard/src/components/usage/state/usageActions.ts +439 -0
- package/templates/shared/dashboard/src/components/usage/state/usageStore.ts +376 -0
- package/templates/shared/dashboard/src/components/usage/types.ts +283 -0
- package/templates/shared/dashboard/src/components/usage/usage-colors.ts +292 -0
- package/templates/shared/dashboard/src/pages/api/usage/ai-models.ts +235 -0
- package/templates/shared/dashboard/src/pages/api/usage/billing-context.ts +296 -0
- package/templates/shared/scripts/test-telemetry-flow.ts +464 -0
- package/templates/shared/tests/e2e/usage-export.test.ts +784 -0
- package/templates/shared/tests/e2e/usage-mobile.test.ts +531 -0
- package/templates/standard/dashboard/src/components/errors/PriorityBadge.astro +27 -0
- package/templates/standard/dashboard/src/components/infrastructure/HealthchecksStatus.tsx +293 -0
- package/templates/standard/dashboard/src/components/infrastructure/InfrastructureTabs.tsx +268 -0
- package/templates/standard/dashboard/src/pages/analytics.astro +64 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/alerts.ts +85 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/healthchecks/[id]/flips.ts +110 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/healthchecks.ts +101 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/uptime/[id]/response-times.ts +121 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/uptime.ts +89 -0
- package/templates/standard/dashboard/src/pages/api/test/service-auth.ts +178 -0
- package/templates/standard/tests/integration/connectors.test.ts +241 -0
- package/templates/standard/tests/integration/github-monitor.test.ts +143 -0
- package/templates/standard/tests/integration/ingestion.test.ts +211 -0
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Usage Dashboard - Mobile Responsive Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests mobile-specific behaviour of the Usage Dashboard:
|
|
5
|
+
* - Card view data transformation
|
|
6
|
+
* - Mobile layout data structure
|
|
7
|
+
* - Touch-friendly element properties
|
|
8
|
+
* - Responsive breakpoint logic
|
|
9
|
+
*
|
|
10
|
+
* Note: These tests verify data and logic for mobile views.
|
|
11
|
+
* Visual/CSS testing would require Playwright with browser context.
|
|
12
|
+
*
|
|
13
|
+
* @module tests/e2e/usage-mobile
|
|
14
|
+
* @created 2026-01-05
|
|
15
|
+
* @task task-17.24 - Mobile responsive tests
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { describe, it, expect } from 'vitest';
|
|
19
|
+
import {
|
|
20
|
+
type UnifiedResource,
|
|
21
|
+
type ResourceType,
|
|
22
|
+
type ResourceStatus,
|
|
23
|
+
RESOURCE_TYPE_ICONS,
|
|
24
|
+
RESOURCE_TYPE_LABELS,
|
|
25
|
+
STATUS_COLOURS,
|
|
26
|
+
formatCurrency,
|
|
27
|
+
formatDeltaPct,
|
|
28
|
+
getDeltaClass,
|
|
29
|
+
} from '../../dashboard/src/components/usage/types';
|
|
30
|
+
import { filterResources, sortResources } from '../../dashboard/src/components/usage/transformers';
|
|
31
|
+
|
|
32
|
+
// Test data for mobile view scenarios
|
|
33
|
+
const createMobileTestResources = (): UnifiedResource[] => [
|
|
34
|
+
{
|
|
35
|
+
id: 'worker-brand-copilot-api',
|
|
36
|
+
name: 'brand-copilot-api',
|
|
37
|
+
type: 'worker',
|
|
38
|
+
project: 'brand-copilot',
|
|
39
|
+
usage: { value: 100000, unit: 'requests', formatted: '100K' },
|
|
40
|
+
costCurrent: 5.0,
|
|
41
|
+
costPrior: 4.0,
|
|
42
|
+
costDelta: 1.0,
|
|
43
|
+
costDeltaPct: 25,
|
|
44
|
+
status: 'healthy',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: 'd1-brand-copilot-db',
|
|
48
|
+
name: 'brand-copilot-db',
|
|
49
|
+
type: 'd1',
|
|
50
|
+
project: 'brand-copilot',
|
|
51
|
+
usage: { value: 500000, unit: 'rows read', formatted: '500K' },
|
|
52
|
+
costCurrent: 2.5,
|
|
53
|
+
costPrior: 2.0,
|
|
54
|
+
costDelta: 0.5,
|
|
55
|
+
costDeltaPct: 25,
|
|
56
|
+
status: 'warning',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: 'kv-platform-cache',
|
|
60
|
+
name: 'platform-cache',
|
|
61
|
+
type: 'kv',
|
|
62
|
+
project: 'platform',
|
|
63
|
+
usage: { value: 50000, unit: 'operations', formatted: '50K' },
|
|
64
|
+
costCurrent: 1.0,
|
|
65
|
+
costPrior: 0,
|
|
66
|
+
costDelta: 1.0,
|
|
67
|
+
costDeltaPct: 'NEW' as const,
|
|
68
|
+
status: 'healthy',
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: 'r2-ai-logs',
|
|
72
|
+
name: 'brand-copilot-ai-logs',
|
|
73
|
+
type: 'r2',
|
|
74
|
+
project: 'brand-copilot',
|
|
75
|
+
// Uses decimal GB (1 GB = 1,000,000,000 bytes) to match Cloudflare billing
|
|
76
|
+
usage: { value: 1000000000, unit: 'bytes', formatted: '1.00 GB' },
|
|
77
|
+
costCurrent: 3.0,
|
|
78
|
+
costPrior: 3.5,
|
|
79
|
+
costDelta: -0.5,
|
|
80
|
+
costDeltaPct: -14.3,
|
|
81
|
+
status: 'healthy',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: 'worker-critical-service',
|
|
85
|
+
name: 'critical-service',
|
|
86
|
+
type: 'worker',
|
|
87
|
+
project: 'platform',
|
|
88
|
+
usage: { value: 25000, unit: 'requests', formatted: '25K' },
|
|
89
|
+
costCurrent: 8.5,
|
|
90
|
+
costPrior: 5.0,
|
|
91
|
+
costDelta: 3.5,
|
|
92
|
+
costDeltaPct: 70,
|
|
93
|
+
status: 'critical',
|
|
94
|
+
},
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
describe('Mobile Responsive Tests', () => {
|
|
98
|
+
describe('Resource Type Icons and Labels', () => {
|
|
99
|
+
it('provides icons for all resource types', () => {
|
|
100
|
+
const resourceTypes: ResourceType[] = [
|
|
101
|
+
'worker',
|
|
102
|
+
'd1',
|
|
103
|
+
'kv',
|
|
104
|
+
'r2',
|
|
105
|
+
'vectorize',
|
|
106
|
+
'pages',
|
|
107
|
+
'queues',
|
|
108
|
+
'workflows',
|
|
109
|
+
'do',
|
|
110
|
+
'ai-gateway',
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
resourceTypes.forEach((type) => {
|
|
114
|
+
expect(RESOURCE_TYPE_ICONS[type]).toBeDefined();
|
|
115
|
+
expect(typeof RESOURCE_TYPE_ICONS[type]).toBe('string');
|
|
116
|
+
expect(RESOURCE_TYPE_ICONS[type].length).toBeGreaterThan(0);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('provides labels for all resource types', () => {
|
|
121
|
+
const resourceTypes: ResourceType[] = [
|
|
122
|
+
'worker',
|
|
123
|
+
'd1',
|
|
124
|
+
'kv',
|
|
125
|
+
'r2',
|
|
126
|
+
'vectorize',
|
|
127
|
+
'pages',
|
|
128
|
+
'queues',
|
|
129
|
+
'workflows',
|
|
130
|
+
'do',
|
|
131
|
+
'ai-gateway',
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
resourceTypes.forEach((type) => {
|
|
135
|
+
expect(RESOURCE_TYPE_LABELS[type]).toBeDefined();
|
|
136
|
+
expect(typeof RESOURCE_TYPE_LABELS[type]).toBe('string');
|
|
137
|
+
expect(RESOURCE_TYPE_LABELS[type].length).toBeGreaterThan(0);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('labels are human-readable (not raw type names)', () => {
|
|
142
|
+
expect(RESOURCE_TYPE_LABELS['d1']).toBe('D1');
|
|
143
|
+
expect(RESOURCE_TYPE_LABELS['kv']).toBe('KV');
|
|
144
|
+
expect(RESOURCE_TYPE_LABELS['r2']).toBe('R2');
|
|
145
|
+
expect(RESOURCE_TYPE_LABELS['do']).toBe('Durable Objects');
|
|
146
|
+
expect(RESOURCE_TYPE_LABELS['ai-gateway']).toBe('AI Gateway');
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('Status Colours', () => {
|
|
151
|
+
it('provides colours for all status levels', () => {
|
|
152
|
+
const statuses: ResourceStatus[] = ['healthy', 'warning', 'high', 'critical'];
|
|
153
|
+
|
|
154
|
+
statuses.forEach((status) => {
|
|
155
|
+
expect(STATUS_COLOURS[status]).toBeDefined();
|
|
156
|
+
expect(STATUS_COLOURS[status]).toMatch(/^#[0-9A-Fa-f]{6}$/);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('uses appropriate colour severity', () => {
|
|
161
|
+
// Healthy should be green-ish
|
|
162
|
+
expect(STATUS_COLOURS.healthy).toBe('#10B981');
|
|
163
|
+
|
|
164
|
+
// Warning should be yellow/amber
|
|
165
|
+
expect(STATUS_COLOURS.warning).toBe('#F59E0B');
|
|
166
|
+
|
|
167
|
+
// High should be orange
|
|
168
|
+
expect(STATUS_COLOURS.high).toBe('#F97316');
|
|
169
|
+
|
|
170
|
+
// Critical should be red
|
|
171
|
+
expect(STATUS_COLOURS.critical).toBe('#EF4444');
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe('Currency Formatting for Mobile', () => {
|
|
176
|
+
it('formats zero as $0.00', () => {
|
|
177
|
+
expect(formatCurrency(0)).toBe('$0.00');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('formats small amounts with < symbol', () => {
|
|
181
|
+
expect(formatCurrency(0.005)).toBe('< $0.01');
|
|
182
|
+
expect(formatCurrency(0.001)).toBe('< $0.01');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('formats normal amounts with 2 decimal places', () => {
|
|
186
|
+
expect(formatCurrency(5)).toBe('$5.00');
|
|
187
|
+
expect(formatCurrency(5.5)).toBe('$5.50');
|
|
188
|
+
expect(formatCurrency(5.556)).toBe('$5.56');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('keeps amounts compact for mobile display', () => {
|
|
192
|
+
// All formatted values should be reasonably short
|
|
193
|
+
const testAmounts = [0, 0.001, 1.5, 10.99, 100.5, 999.99];
|
|
194
|
+
testAmounts.forEach((amount) => {
|
|
195
|
+
const formatted = formatCurrency(amount);
|
|
196
|
+
expect(formatted.length).toBeLessThan(15);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe('Delta Percentage Formatting for Mobile', () => {
|
|
202
|
+
it('formats NEW as readable string', () => {
|
|
203
|
+
expect(formatDeltaPct('NEW')).toBe('NEW');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('formats null as em dash', () => {
|
|
207
|
+
expect(formatDeltaPct(null)).toBe('—');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('formats zero without sign', () => {
|
|
211
|
+
expect(formatDeltaPct(0)).toBe('0%');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('formats positive with plus sign', () => {
|
|
215
|
+
expect(formatDeltaPct(25)).toBe('+25.0%');
|
|
216
|
+
expect(formatDeltaPct(100)).toBe('+100.0%');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('formats negative with minus sign', () => {
|
|
220
|
+
expect(formatDeltaPct(-15)).toBe('-15.0%');
|
|
221
|
+
expect(formatDeltaPct(-50)).toBe('-50.0%');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('outputs compact strings for mobile', () => {
|
|
225
|
+
const testValues = [0, 25, -25, 100, -100, 'NEW' as const, null];
|
|
226
|
+
testValues.forEach((value) => {
|
|
227
|
+
const formatted = formatDeltaPct(value);
|
|
228
|
+
expect(formatted.length).toBeLessThan(10);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe('Delta Class for Visual Styling', () => {
|
|
234
|
+
it('returns delta-new for NEW resources', () => {
|
|
235
|
+
expect(getDeltaClass('NEW')).toBe('delta-new');
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('returns delta-neutral for zero or null', () => {
|
|
239
|
+
expect(getDeltaClass(0)).toBe('delta-neutral');
|
|
240
|
+
expect(getDeltaClass(null)).toBe('delta-neutral');
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('returns delta-up for positive changes', () => {
|
|
244
|
+
expect(getDeltaClass(25)).toBe('delta-up');
|
|
245
|
+
expect(getDeltaClass(100)).toBe('delta-up');
|
|
246
|
+
expect(getDeltaClass(0.1)).toBe('delta-up');
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('returns delta-down for negative changes', () => {
|
|
250
|
+
expect(getDeltaClass(-10)).toBe('delta-down');
|
|
251
|
+
expect(getDeltaClass(-50)).toBe('delta-down');
|
|
252
|
+
expect(getDeltaClass(-0.1)).toBe('delta-down');
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
describe('Mobile Card Data Structure', () => {
|
|
257
|
+
it('all resources have required mobile card fields', () => {
|
|
258
|
+
const resources = createMobileTestResources();
|
|
259
|
+
|
|
260
|
+
resources.forEach((resource) => {
|
|
261
|
+
// Header row data
|
|
262
|
+
expect(resource.name).toBeDefined();
|
|
263
|
+
expect(typeof resource.name).toBe('string');
|
|
264
|
+
expect(resource.type).toBeDefined();
|
|
265
|
+
expect(resource.status).toBeDefined();
|
|
266
|
+
|
|
267
|
+
// Body metrics
|
|
268
|
+
expect(resource.project).toBeDefined();
|
|
269
|
+
expect(resource.usage).toBeDefined();
|
|
270
|
+
expect(resource.usage.formatted).toBeDefined();
|
|
271
|
+
expect(resource.usage.unit).toBeDefined();
|
|
272
|
+
expect(resource.costCurrent).toBeDefined();
|
|
273
|
+
expect(resource.costDeltaPct).toBeDefined();
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('resource names are not too long for mobile', () => {
|
|
278
|
+
const resources = createMobileTestResources();
|
|
279
|
+
|
|
280
|
+
resources.forEach((resource) => {
|
|
281
|
+
// Names should be reasonable length (can be truncated with ellipsis)
|
|
282
|
+
// but very long names could cause layout issues
|
|
283
|
+
expect(resource.name.length).toBeLessThan(100);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('formatted usage values are compact', () => {
|
|
288
|
+
const resources = createMobileTestResources();
|
|
289
|
+
|
|
290
|
+
resources.forEach((resource) => {
|
|
291
|
+
// Formatted values should be human-readable and compact
|
|
292
|
+
expect(resource.usage.formatted.length).toBeLessThan(20);
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
describe('Mobile Sorting Behaviour', () => {
|
|
298
|
+
it('sorts by cost for easy comparison', () => {
|
|
299
|
+
const resources = createMobileTestResources();
|
|
300
|
+
const sorted = sortResources(resources, 'costCurrent', 'desc');
|
|
301
|
+
|
|
302
|
+
// Highest cost first
|
|
303
|
+
expect(sorted[0].costCurrent).toBe(8.5);
|
|
304
|
+
expect(sorted[sorted.length - 1].costCurrent).toBe(1.0);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('sorts by status severity for quick triage', () => {
|
|
308
|
+
const resources = createMobileTestResources();
|
|
309
|
+
const sorted = sortResources(resources, 'status', 'desc');
|
|
310
|
+
|
|
311
|
+
// Critical first
|
|
312
|
+
expect(sorted[0].status).toBe('critical');
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('sorts by name alphabetically', () => {
|
|
316
|
+
const resources = createMobileTestResources();
|
|
317
|
+
const sorted = sortResources(resources, 'name', 'asc');
|
|
318
|
+
|
|
319
|
+
// Alphabetical order
|
|
320
|
+
for (let i = 0; i < sorted.length - 1; i++) {
|
|
321
|
+
expect(sorted[i].name.localeCompare(sorted[i + 1].name)).toBeLessThanOrEqual(0);
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('handles NEW in delta percentage sorting', () => {
|
|
326
|
+
const resources = createMobileTestResources();
|
|
327
|
+
const sorted = sortResources(resources, 'costDeltaPct', 'desc');
|
|
328
|
+
|
|
329
|
+
// NEW should appear first in descending order
|
|
330
|
+
expect(sorted[0].costDeltaPct).toBe('NEW');
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
describe('Mobile Filtering Behaviour', () => {
|
|
335
|
+
it('filters by project for focused view', () => {
|
|
336
|
+
const resources = createMobileTestResources();
|
|
337
|
+
const filtered = filterResources(resources, { project: 'brand-copilot' });
|
|
338
|
+
|
|
339
|
+
expect(filtered.length).toBe(3);
|
|
340
|
+
expect(filtered.every((r) => r.project === 'brand-copilot')).toBe(true);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('filters by service type', () => {
|
|
344
|
+
const resources = createMobileTestResources();
|
|
345
|
+
const filtered = filterResources(resources, { serviceTypes: ['worker'] });
|
|
346
|
+
|
|
347
|
+
expect(filtered.length).toBe(2);
|
|
348
|
+
expect(filtered.every((r) => r.type === 'worker')).toBe(true);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it('filters by non-zero cost for mobile billing focus', () => {
|
|
352
|
+
const resources = createMobileTestResources();
|
|
353
|
+
const filtered = filterResources(resources, { nonZeroCost: true });
|
|
354
|
+
|
|
355
|
+
expect(filtered.length).toBe(5); // All have costs > 0
|
|
356
|
+
expect(filtered.every((r) => r.costCurrent > 0)).toBe(true);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('filters by only changed resources', () => {
|
|
360
|
+
const resources = createMobileTestResources();
|
|
361
|
+
const filtered = filterResources(resources, { onlyChanged: true });
|
|
362
|
+
|
|
363
|
+
// Resources with > 5% change or NEW
|
|
364
|
+
filtered.forEach((r) => {
|
|
365
|
+
if (typeof r.costDeltaPct === 'number') {
|
|
366
|
+
expect(Math.abs(r.costDeltaPct)).toBeGreaterThan(5);
|
|
367
|
+
} else {
|
|
368
|
+
expect(r.costDeltaPct).toBe('NEW');
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('supports search query for quick find', () => {
|
|
374
|
+
const resources = createMobileTestResources();
|
|
375
|
+
const filtered = filterResources(resources, { searchQuery: 'brand' });
|
|
376
|
+
|
|
377
|
+
expect(filtered.length).toBe(3);
|
|
378
|
+
expect(filtered.every((r) => r.name.toLowerCase().includes('brand'))).toBe(true);
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
describe('Mobile Touch Target Requirements', () => {
|
|
383
|
+
// Note: These are data validation tests. CSS would need browser testing.
|
|
384
|
+
// The component uses tabindex=0 on rows, making them focusable/clickable.
|
|
385
|
+
|
|
386
|
+
it('resources have unique IDs for click handling', () => {
|
|
387
|
+
const resources = createMobileTestResources();
|
|
388
|
+
const ids = resources.map((r) => r.id);
|
|
389
|
+
const uniqueIds = new Set(ids);
|
|
390
|
+
|
|
391
|
+
expect(uniqueIds.size).toBe(ids.length);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('resources have type for icon display', () => {
|
|
395
|
+
const resources = createMobileTestResources();
|
|
396
|
+
|
|
397
|
+
resources.forEach((resource) => {
|
|
398
|
+
expect(RESOURCE_TYPE_ICONS[resource.type]).toBeDefined();
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('resources have status for colour indicator', () => {
|
|
403
|
+
const resources = createMobileTestResources();
|
|
404
|
+
|
|
405
|
+
resources.forEach((resource) => {
|
|
406
|
+
expect(STATUS_COLOURS[resource.status]).toBeDefined();
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
describe('Mobile Breakpoint Data Compatibility', () => {
|
|
412
|
+
// Verify data works for both table and card views
|
|
413
|
+
|
|
414
|
+
it('all fields used in table are available for cards', () => {
|
|
415
|
+
const resources = createMobileTestResources();
|
|
416
|
+
const tableFields = [
|
|
417
|
+
'name',
|
|
418
|
+
'type',
|
|
419
|
+
'project',
|
|
420
|
+
'usage',
|
|
421
|
+
'costCurrent',
|
|
422
|
+
'costDeltaPct',
|
|
423
|
+
'status',
|
|
424
|
+
];
|
|
425
|
+
|
|
426
|
+
resources.forEach((resource) => {
|
|
427
|
+
tableFields.forEach((field) => {
|
|
428
|
+
expect(resource).toHaveProperty(field);
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it('usage has all subfields for mobile display', () => {
|
|
434
|
+
const resources = createMobileTestResources();
|
|
435
|
+
|
|
436
|
+
resources.forEach((resource) => {
|
|
437
|
+
expect(resource.usage).toHaveProperty('value');
|
|
438
|
+
expect(resource.usage).toHaveProperty('unit');
|
|
439
|
+
expect(resource.usage).toHaveProperty('formatted');
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
describe('Edge Cases for Mobile Display', () => {
|
|
445
|
+
it('handles empty resource list', () => {
|
|
446
|
+
const filtered = filterResources([], {});
|
|
447
|
+
expect(filtered).toHaveLength(0);
|
|
448
|
+
|
|
449
|
+
const sorted = sortResources([], 'costCurrent', 'desc');
|
|
450
|
+
expect(sorted).toHaveLength(0);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it('handles single resource', () => {
|
|
454
|
+
const resources = createMobileTestResources().slice(0, 1);
|
|
455
|
+
|
|
456
|
+
const sorted = sortResources(resources, 'costCurrent', 'desc');
|
|
457
|
+
expect(sorted).toHaveLength(1);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it('handles resource with null delta percentage', () => {
|
|
461
|
+
const resource: UnifiedResource = {
|
|
462
|
+
id: 'test',
|
|
463
|
+
name: 'test-resource',
|
|
464
|
+
type: 'worker',
|
|
465
|
+
project: 'test',
|
|
466
|
+
usage: { value: 100, unit: 'requests', formatted: '100' },
|
|
467
|
+
costCurrent: 1.0,
|
|
468
|
+
costPrior: 0,
|
|
469
|
+
costDelta: 1.0,
|
|
470
|
+
costDeltaPct: null,
|
|
471
|
+
status: 'healthy',
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
expect(formatDeltaPct(resource.costDeltaPct)).toBe('—');
|
|
475
|
+
expect(getDeltaClass(resource.costDeltaPct)).toBe('delta-neutral');
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it('handles very long resource names', () => {
|
|
479
|
+
const longName = 'this-is-a-very-long-resource-name-that-might-overflow-on-mobile-devices';
|
|
480
|
+
const formatted = longName; // Component truncates with CSS text-overflow: ellipsis
|
|
481
|
+
|
|
482
|
+
expect(formatted.length).toBeGreaterThan(50); // Just verify long names are allowed
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it('handles large cost values', () => {
|
|
486
|
+
const largeCost = 9999.99;
|
|
487
|
+
const formatted = formatCurrency(largeCost);
|
|
488
|
+
|
|
489
|
+
expect(formatted).toBe('$9999.99');
|
|
490
|
+
expect(formatted.length).toBeLessThan(15);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it('handles extreme percentage changes', () => {
|
|
494
|
+
expect(formatDeltaPct(500)).toBe('+500.0%');
|
|
495
|
+
expect(formatDeltaPct(-99)).toBe('-99.0%');
|
|
496
|
+
expect(formatDeltaPct(1000)).toBe('+1000.0%');
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
describe('Accessibility Data for Mobile', () => {
|
|
501
|
+
it('status values are screenreader-friendly', () => {
|
|
502
|
+
const statuses: ResourceStatus[] = ['healthy', 'warning', 'high', 'critical'];
|
|
503
|
+
|
|
504
|
+
statuses.forEach((status) => {
|
|
505
|
+
// Status values should be words, not codes
|
|
506
|
+
expect(status.length).toBeGreaterThan(2);
|
|
507
|
+
expect(/^[a-z]+$/i.test(status)).toBe(true);
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
it('type labels are screenreader-friendly', () => {
|
|
512
|
+
Object.values(RESOURCE_TYPE_LABELS).forEach((label) => {
|
|
513
|
+
// Labels should be readable words/phrases
|
|
514
|
+
expect(label.length).toBeGreaterThan(0);
|
|
515
|
+
expect(/^[A-Za-z0-9\s]+$/.test(label)).toBe(true);
|
|
516
|
+
});
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
it('formatted values include context', () => {
|
|
520
|
+
const resources = createMobileTestResources();
|
|
521
|
+
|
|
522
|
+
resources.forEach((resource) => {
|
|
523
|
+
// Formatted values should be numeric-looking for numbers
|
|
524
|
+
expect(resource.usage.formatted).toBeDefined();
|
|
525
|
+
|
|
526
|
+
// Units provide context
|
|
527
|
+
expect(resource.usage.unit.length).toBeGreaterThan(0);
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Priority Badge Component
|
|
4
|
+
* Displays P0-P4 priority with appropriate colour coding
|
|
5
|
+
*/
|
|
6
|
+
interface Props {
|
|
7
|
+
priority: string;
|
|
8
|
+
size?: 'sm' | 'md';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { priority, size = 'md' } = Astro.props;
|
|
12
|
+
|
|
13
|
+
const colours: Record<string, string> = {
|
|
14
|
+
P0: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300 border-red-200 dark:border-red-800',
|
|
15
|
+
P1: 'bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300 border-orange-200 dark:border-orange-800',
|
|
16
|
+
P2: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300 border-yellow-200 dark:border-yellow-800',
|
|
17
|
+
P3: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300 border-blue-200 dark:border-blue-800',
|
|
18
|
+
P4: 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-300 border-gray-200 dark:border-gray-800',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const sizeClasses = size === 'sm' ? 'px-1.5 py-0.5 text-xs' : 'px-2 py-1 text-sm';
|
|
22
|
+
const colourClass = colours[priority] || colours.P4;
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
<span class={`inline-flex items-center font-semibold rounded border ${colourClass} ${sizeClasses}`}>
|
|
26
|
+
{priority}
|
|
27
|
+
</span>
|