@happyvertical/smrt-projects 0.52.0 → 0.53.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/AGENTS.md +20 -9
- package/README.md +32 -1
- package/SERVICE_TIME_MIGRATION.md +45 -3
- package/dist/chunks/{Issue-Dwyg6-_O.js → Issue-DG5UNAGa.js} +6 -5
- package/dist/chunks/Issue-DG5UNAGa.js.map +1 -0
- package/dist/chunks/{PullRequest-D27EkBdR.js → PullRequest-CN7LpEl4.js} +2 -2
- package/dist/chunks/{PullRequest-D27EkBdR.js.map → PullRequest-CN7LpEl4.js.map} +1 -1
- package/dist/chunks/repository-client-scope-CAnWW7-r.js +50 -0
- package/dist/chunks/repository-client-scope-CAnWW7-r.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +131 -591
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +3 -1484
- package/dist/models/Issue.d.ts.map +1 -1
- package/dist/models/Repository.d.ts.map +1 -1
- package/dist/models/service-evidence.d.ts +13 -92
- package/dist/models/service-evidence.d.ts.map +1 -1
- package/dist/playground.js +2 -2
- package/dist/playground.js.map +1 -1
- package/dist/repository-client-scope.d.ts +41 -0
- package/dist/repository-client-scope.d.ts.map +1 -0
- package/dist/services/service-evidence-service.d.ts +6 -55
- package/dist/services/service-evidence-service.d.ts.map +1 -1
- package/dist/services/subscription-commercial-resolver.d.ts +1 -1
- package/dist/services/subscription-commercial-resolver.d.ts.map +1 -1
- package/dist/smrt-knowledge.json +10 -1068
- package/dist/svelte/components/ApprovalActions.svelte +1 -1
- package/dist/svelte/components/ApprovalActions.svelte.d.ts +1 -1
- package/dist/svelte/components/ApprovalActions.svelte.d.ts.map +1 -1
- package/dist/svelte/i18n.d.ts +0 -9
- package/dist/svelte/i18n.d.ts.map +1 -1
- package/dist/svelte/i18n.js +0 -9
- package/dist/svelte/index.d.ts +2 -8
- package/dist/svelte/index.d.ts.map +1 -1
- package/dist/svelte/index.js +3 -4
- package/dist/svelte/playground.d.ts +7 -2
- package/dist/svelte/playground.d.ts.map +1 -1
- package/dist/svelte/playground.js +7 -2
- package/dist/svelte/utils.d.ts +4 -48
- package/dist/svelte/utils.d.ts.map +1 -1
- package/dist/svelte/utils.js +4 -56
- package/dist/types.d.ts +1 -10
- package/dist/types.d.ts.map +1 -1
- package/package.json +19 -19
- package/dist/chunks/Issue-Dwyg6-_O.js.map +0 -1
- package/dist/svelte/components/DurationDisplay.svelte +0 -81
- package/dist/svelte/components/DurationDisplay.svelte.d.ts +0 -20
- package/dist/svelte/components/DurationDisplay.svelte.d.ts.map +0 -1
- package/dist/svelte/components/TimeEntryCard.svelte +0 -304
- package/dist/svelte/components/TimeEntryCard.svelte.d.ts +0 -25
- package/dist/svelte/components/TimeEntryCard.svelte.d.ts.map +0 -1
- package/dist/svelte/components/TimeEntryList.svelte +0 -360
- package/dist/svelte/components/TimeEntryList.svelte.d.ts +0 -24
- package/dist/svelte/components/TimeEntryList.svelte.d.ts.map +0 -1
- package/dist/svelte/components/TimeSummary.svelte +0 -176
- package/dist/svelte/components/TimeSummary.svelte.d.ts +0 -30
- package/dist/svelte/components/TimeSummary.svelte.d.ts.map +0 -1
- package/dist/svelte/components/__tests__/ApprovalActions.test.js +0 -41
- package/dist/svelte/components/__tests__/BulkActions.test.js +0 -46
- package/dist/svelte/components/__tests__/DeliverySurfaces.test.js +0 -98
- package/dist/svelte/components/__tests__/DevelopmentRequest.test.js +0 -52
- package/dist/svelte/components/__tests__/DurationDisplay.test.js +0 -57
- package/dist/svelte/components/__tests__/ProjectBoard.test.js +0 -156
- package/dist/svelte/components/__tests__/RejectDialog.test.js +0 -45
- package/dist/svelte/components/__tests__/TimeEntryCard.test.js +0 -45
- package/dist/svelte/components/__tests__/TimeEntryList.test.js +0 -50
- package/dist/svelte/components/__tests__/TimeSummary.test.js +0 -39
- package/dist/svelte/components/utils.d.ts +0 -51
- package/dist/svelte/components/utils.d.ts.map +0 -1
- package/dist/svelte/components/utils.js +0 -58
- package/dist/svelte/utils.test.js +0 -44
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"TimeEntryList.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/TimeEntryList.svelte.ts"],"names":[],"mappings":"AASA,OAAO,EACL,KAAK,QAAQ,EAKb,KAAK,SAAS,EACf,MAAM,YAAY,CAAC;AAGpB,wCAAwC;AACxC,MAAM,WAAW,KAAK;IACpB,2CAA2C;IAC3C,OAAO,EAAE,SAAS,EAAE,CAAC;IACrB,8CAA8C;IAC9C,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,iDAAiD;IACjD,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,0CAA0C;IAC1C,iBAAiB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;IAC5C,oDAAoD;IACpD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gDAAgD;IAChD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4CAA4C;IAC5C,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,iEAAiE;IACjE,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,OAAO,CAAC;CAC3C;AA2ID,QAAA,MAAM,aAAa,2CAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
* TimeSummary - Summary statistics for time entries
|
|
4
|
-
* Shows total hours, amounts, and pending items
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { useI18n } from '@happyvertical/smrt-ui/i18n';
|
|
8
|
-
import { M } from '../i18n.js';
|
|
9
|
-
import { type Currency, formatCurrency, formatHours } from './utils.js';
|
|
10
|
-
|
|
11
|
-
/** Props for TimeSummary component */
|
|
12
|
-
export interface Props {
|
|
13
|
-
/** Total hours across all time entries. */
|
|
14
|
-
totalHours: number;
|
|
15
|
-
/** Total charged or invoiced amount. */
|
|
16
|
-
totalAmount: number;
|
|
17
|
-
/** Hours awaiting approval. */
|
|
18
|
-
pendingHours?: number;
|
|
19
|
-
/** Amount awaiting approval. */
|
|
20
|
-
pendingAmount?: number;
|
|
21
|
-
/** Hours that have been approved. */
|
|
22
|
-
approvedHours?: number;
|
|
23
|
-
/** Amount that has been approved. */
|
|
24
|
-
approvedAmount?: number;
|
|
25
|
-
/** Number of time entries included in the summary. */
|
|
26
|
-
entryCount?: number;
|
|
27
|
-
/** Currency code for formatting amounts. */
|
|
28
|
-
currency?: Currency;
|
|
29
|
-
/** Shows the pending hours and amount card when there are pending items. */
|
|
30
|
-
showPending?: boolean;
|
|
31
|
-
/** Shows the approved hours and amount card when there are approved items. */
|
|
32
|
-
showApproved?: boolean;
|
|
33
|
-
/** Arranges summary cards in a grid or flexbox layout. */
|
|
34
|
-
layout?: 'horizontal' | 'grid';
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
let {
|
|
38
|
-
totalHours,
|
|
39
|
-
totalAmount,
|
|
40
|
-
pendingHours = 0,
|
|
41
|
-
pendingAmount = 0,
|
|
42
|
-
approvedHours = 0,
|
|
43
|
-
approvedAmount = 0,
|
|
44
|
-
entryCount,
|
|
45
|
-
currency = 'CAD',
|
|
46
|
-
showPending = true,
|
|
47
|
-
showApproved = false,
|
|
48
|
-
layout = 'grid',
|
|
49
|
-
}: Props = $props();
|
|
50
|
-
|
|
51
|
-
const { t } = useI18n();
|
|
52
|
-
</script>
|
|
53
|
-
|
|
54
|
-
<div class="time-summary" class:horizontal={layout === 'horizontal'}>
|
|
55
|
-
<div class="summary-card">
|
|
56
|
-
<span class="label">{t(M['projects.time_summary.total_hours'])}</span>
|
|
57
|
-
<span class="value">{formatHours(totalHours)}</span>
|
|
58
|
-
{#if entryCount !== undefined}
|
|
59
|
-
<span class="count">{entryCount} {entryCount === 1 ? 'entry' : 'entries'}</span>
|
|
60
|
-
{/if}
|
|
61
|
-
</div>
|
|
62
|
-
|
|
63
|
-
<div class="summary-card">
|
|
64
|
-
<span class="label">{t(M['projects.time_summary.total_value'])}</span>
|
|
65
|
-
<span class="value">{formatCurrency(totalAmount, currency)}</span>
|
|
66
|
-
</div>
|
|
67
|
-
|
|
68
|
-
{#if showPending && (pendingHours > 0 || pendingAmount > 0)}
|
|
69
|
-
<div class="summary-card highlight">
|
|
70
|
-
<span class="label">{t(M['projects.time_summary.pending_approval'])}</span>
|
|
71
|
-
<span class="value">{formatHours(pendingHours)}</span>
|
|
72
|
-
<span class="sub-value">{formatCurrency(pendingAmount, currency)}</span>
|
|
73
|
-
</div>
|
|
74
|
-
{/if}
|
|
75
|
-
|
|
76
|
-
{#if showApproved && (approvedHours > 0 || approvedAmount > 0)}
|
|
77
|
-
<div class="summary-card success">
|
|
78
|
-
<span class="label">Approved</span>
|
|
79
|
-
<span class="value">{formatHours(approvedHours)}</span>
|
|
80
|
-
<span class="sub-value">{formatCurrency(approvedAmount, currency)}</span>
|
|
81
|
-
</div>
|
|
82
|
-
{/if}
|
|
83
|
-
</div>
|
|
84
|
-
|
|
85
|
-
<style>
|
|
86
|
-
.time-summary {
|
|
87
|
-
display: grid;
|
|
88
|
-
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
|
89
|
-
gap: 1rem;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
.time-summary.horizontal {
|
|
93
|
-
display: flex;
|
|
94
|
-
flex-wrap: wrap;
|
|
95
|
-
gap: 1rem;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
.time-summary.horizontal .summary-card {
|
|
99
|
-
flex: 1 1 140px;
|
|
100
|
-
min-width: 140px;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
.summary-card {
|
|
104
|
-
background: var(--smrt-color-surface);
|
|
105
|
-
border: 1px solid var(--smrt-color-outline-variant);
|
|
106
|
-
border-radius: var(--smrt-radius-medium, 12px);
|
|
107
|
-
padding: 1rem;
|
|
108
|
-
display: flex;
|
|
109
|
-
flex-direction: column;
|
|
110
|
-
gap: 0.25rem;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
.summary-card.highlight {
|
|
114
|
-
background: var(--smrt-color-tertiary-container);
|
|
115
|
-
border-color: var(--smrt-color-tertiary);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
.summary-card.success {
|
|
119
|
-
background: var(--smrt-color-primary-container);
|
|
120
|
-
border-color: var(--smrt-color-primary);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
.label {
|
|
124
|
-
font-size: var(--smrt-typography-label-small-size, 0.75rem);
|
|
125
|
-
font-weight: var(--smrt-typography-label-small-weight, 500);
|
|
126
|
-
color: var(--smrt-color-on-surface-variant);
|
|
127
|
-
text-transform: uppercase;
|
|
128
|
-
letter-spacing: var(--smrt-typography-label-small-tracking, 0.5px);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
.value {
|
|
132
|
-
font-size: var(--smrt-typography-headline-small-size, 1.5rem);
|
|
133
|
-
font-weight: var(--smrt-typography-headline-small-weight, 400);
|
|
134
|
-
color: var(--smrt-color-on-surface);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
.sub-value {
|
|
138
|
-
font-size: var(--smrt-typography-body-medium-size, 0.875rem);
|
|
139
|
-
color: var(--smrt-color-on-surface-variant);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
.count {
|
|
143
|
-
font-size: var(--smrt-typography-body-small-size, 0.75rem);
|
|
144
|
-
color: var(--smrt-color-on-surface-variant);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
.highlight .label {
|
|
148
|
-
color: var(--smrt-color-on-tertiary-container);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
.highlight .value {
|
|
152
|
-
color: var(--smrt-color-tertiary);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
.highlight .sub-value {
|
|
156
|
-
color: var(--smrt-color-on-tertiary-container);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
.success .label {
|
|
160
|
-
color: var(--smrt-color-on-primary-container);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
.success .value {
|
|
164
|
-
color: var(--smrt-color-primary);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
.success .sub-value {
|
|
168
|
-
color: var(--smrt-color-on-primary-container);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
@media (max-width: 480px) {
|
|
172
|
-
.time-summary {
|
|
173
|
-
grid-template-columns: 1fr 1fr;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
</style>
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { type Currency } from './utils.js';
|
|
2
|
-
/** Props for TimeSummary component */
|
|
3
|
-
export interface Props {
|
|
4
|
-
/** Total hours across all time entries. */
|
|
5
|
-
totalHours: number;
|
|
6
|
-
/** Total charged or invoiced amount. */
|
|
7
|
-
totalAmount: number;
|
|
8
|
-
/** Hours awaiting approval. */
|
|
9
|
-
pendingHours?: number;
|
|
10
|
-
/** Amount awaiting approval. */
|
|
11
|
-
pendingAmount?: number;
|
|
12
|
-
/** Hours that have been approved. */
|
|
13
|
-
approvedHours?: number;
|
|
14
|
-
/** Amount that has been approved. */
|
|
15
|
-
approvedAmount?: number;
|
|
16
|
-
/** Number of time entries included in the summary. */
|
|
17
|
-
entryCount?: number;
|
|
18
|
-
/** Currency code for formatting amounts. */
|
|
19
|
-
currency?: Currency;
|
|
20
|
-
/** Shows the pending hours and amount card when there are pending items. */
|
|
21
|
-
showPending?: boolean;
|
|
22
|
-
/** Shows the approved hours and amount card when there are approved items. */
|
|
23
|
-
showApproved?: boolean;
|
|
24
|
-
/** Arranges summary cards in a grid or flexbox layout. */
|
|
25
|
-
layout?: 'horizontal' | 'grid';
|
|
26
|
-
}
|
|
27
|
-
declare const TimeSummary: import("svelte").Component<Props, {}, "">;
|
|
28
|
-
type TimeSummary = ReturnType<typeof TimeSummary>;
|
|
29
|
-
export default TimeSummary;
|
|
30
|
-
//# sourceMappingURL=TimeSummary.svelte.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"TimeSummary.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/TimeSummary.svelte.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,KAAK,QAAQ,EAA+B,MAAM,YAAY,CAAC;AAGxE,sCAAsC;AACtC,MAAM,WAAW,KAAK;IACpB,2CAA2C;IAC3C,UAAU,EAAE,MAAM,CAAC;IACnB,wCAAwC;IACxC,WAAW,EAAE,MAAM,CAAC;IACpB,+BAA+B;IAC/B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gCAAgC;IAChC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,qCAAqC;IACrC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,qCAAqC;IACrC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,sDAAsD;IACtD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4CAA4C;IAC5C,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,4EAA4E;IAC5E,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,8EAA8E;IAC9E,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,0DAA0D;IAC1D,MAAM,CAAC,EAAE,YAAY,GAAG,MAAM,CAAC;CAChC;AA6DD,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
/**
|
|
3
|
-
* Component coverage for ApprovalActions via the shared S11 harness (#1416).
|
|
4
|
-
*/
|
|
5
|
-
import { expectNoA11yViolations, render, screen, userEvent, } from '@happyvertical/smrt-vitest/svelte';
|
|
6
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
7
|
-
import ApprovalActions from '../ApprovalActions.svelte';
|
|
8
|
-
describe('ApprovalActions', () => {
|
|
9
|
-
it('offers Submit/Delete for a draft and submits on click', async () => {
|
|
10
|
-
const onsubmit = vi.fn();
|
|
11
|
-
render(ApprovalActions, {
|
|
12
|
-
props: { status: 'draft', onsubmit, ondelete: vi.fn() },
|
|
13
|
-
});
|
|
14
|
-
expect(screen.getByRole('button', { name: 'Submit for Approval' })).toBeInTheDocument();
|
|
15
|
-
await userEvent.click(screen.getByRole('button', { name: 'Submit for Approval' }));
|
|
16
|
-
expect(onsubmit).toHaveBeenCalledTimes(1);
|
|
17
|
-
});
|
|
18
|
-
it('offers Approve/Reject for a submitted entry', async () => {
|
|
19
|
-
const onapprove = vi.fn();
|
|
20
|
-
render(ApprovalActions, {
|
|
21
|
-
props: { status: 'submitted', onapprove, onreject: vi.fn() },
|
|
22
|
-
});
|
|
23
|
-
await userEvent.click(screen.getByRole('button', { name: 'Approve' }));
|
|
24
|
-
expect(onapprove).toHaveBeenCalledTimes(1);
|
|
25
|
-
expect(screen.getByRole('button', { name: 'Reject' })).toBeInTheDocument();
|
|
26
|
-
});
|
|
27
|
-
it('shows the approved status message', () => {
|
|
28
|
-
render(ApprovalActions, { props: { status: 'approved' } });
|
|
29
|
-
expect(screen.getByText('This entry has been approved')).toBeInTheDocument();
|
|
30
|
-
});
|
|
31
|
-
it('is axe-clean', async () => {
|
|
32
|
-
const { container } = render(ApprovalActions, {
|
|
33
|
-
props: {
|
|
34
|
-
status: 'submitted',
|
|
35
|
-
onapprove: vi.fn(),
|
|
36
|
-
onreject: vi.fn(),
|
|
37
|
-
},
|
|
38
|
-
});
|
|
39
|
-
await expectNoA11yViolations(container);
|
|
40
|
-
});
|
|
41
|
-
});
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
/**
|
|
3
|
-
* Component coverage for BulkActions via the shared S11 harness (#1416).
|
|
4
|
-
*/
|
|
5
|
-
import { expectNoA11yViolations, render, screen, userEvent, } from '@happyvertical/smrt-vitest/svelte';
|
|
6
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
7
|
-
import BulkActions from '../BulkActions.svelte';
|
|
8
|
-
const baseProps = (over = {}) => ({
|
|
9
|
-
selectedCount: 3,
|
|
10
|
-
onapprove: vi.fn(),
|
|
11
|
-
onreject: vi.fn(),
|
|
12
|
-
ondelete: vi.fn(),
|
|
13
|
-
onexport: vi.fn(),
|
|
14
|
-
onclear: vi.fn(),
|
|
15
|
-
...over,
|
|
16
|
-
});
|
|
17
|
-
describe('BulkActions', () => {
|
|
18
|
-
it('shows the selection count and bulk action buttons', () => {
|
|
19
|
-
render(BulkActions, { props: baseProps() });
|
|
20
|
-
expect(screen.getByText('3')).toBeInTheDocument();
|
|
21
|
-
expect(screen.getByText('items selected')).toBeInTheDocument();
|
|
22
|
-
expect(screen.getByRole('button', { name: 'Approve All' })).toBeInTheDocument();
|
|
23
|
-
});
|
|
24
|
-
it('invokes onapprove when Approve All is clicked', async () => {
|
|
25
|
-
const onapprove = vi.fn();
|
|
26
|
-
render(BulkActions, { props: baseProps({ onapprove }) });
|
|
27
|
-
await userEvent.click(screen.getByRole('button', { name: 'Approve All' }));
|
|
28
|
-
expect(onapprove).toHaveBeenCalledTimes(1);
|
|
29
|
-
});
|
|
30
|
-
it('clears the selection', async () => {
|
|
31
|
-
const onclear = vi.fn();
|
|
32
|
-
render(BulkActions, { props: baseProps({ onclear }) });
|
|
33
|
-
await userEvent.click(screen.getByRole('button', { name: 'Clear' }));
|
|
34
|
-
expect(onclear).toHaveBeenCalledTimes(1);
|
|
35
|
-
});
|
|
36
|
-
it('renders nothing when no items are selected', () => {
|
|
37
|
-
const { container } = render(BulkActions, {
|
|
38
|
-
props: baseProps({ selectedCount: 0 }),
|
|
39
|
-
});
|
|
40
|
-
expect(container.querySelector('.bulk-actions')).toBeNull();
|
|
41
|
-
});
|
|
42
|
-
it('is axe-clean', async () => {
|
|
43
|
-
const { container } = render(BulkActions, { props: baseProps() });
|
|
44
|
-
await expectNoA11yViolations(container);
|
|
45
|
-
});
|
|
46
|
-
});
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
import { expectNoA11yViolations, render, screen, userEvent, } from '@happyvertical/smrt-vitest/svelte';
|
|
3
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
-
import AssistanceLauncher from '../../AssistanceLauncher.svelte';
|
|
5
|
-
import DevelopmentBoard from '../../DevelopmentBoard.svelte';
|
|
6
|
-
import DevelopmentRequestDetail from '../../DevelopmentRequestDetail.svelte';
|
|
7
|
-
import PreviewApprovalPanel from '../../PreviewApprovalPanel.svelte';
|
|
8
|
-
describe('managed application delivery surfaces', () => {
|
|
9
|
-
it('submits Assistance intake without dropping requester context or evidence', async () => {
|
|
10
|
-
const onsubmit = vi.fn();
|
|
11
|
-
const { container } = render(AssistanceLauncher, {
|
|
12
|
-
props: {
|
|
13
|
-
requesterId: 'requester-1',
|
|
14
|
-
applicationContext: { route: '/invoices' },
|
|
15
|
-
evidence: [{ url: 'https://evidence.invalid/screenshot' }],
|
|
16
|
-
onsubmit,
|
|
17
|
-
},
|
|
18
|
-
});
|
|
19
|
-
await userEvent.type(screen.getByLabelText('Subject'), 'Export failed');
|
|
20
|
-
await userEvent.type(screen.getByLabelText('What do you need?'), 'The CSV export returns an error.');
|
|
21
|
-
await userEvent.click(screen.getByRole('button', { name: 'Send request' }));
|
|
22
|
-
expect(onsubmit).toHaveBeenCalledWith({
|
|
23
|
-
requesterId: 'requester-1',
|
|
24
|
-
subject: 'Export failed',
|
|
25
|
-
applicationContext: { route: '/invoices' },
|
|
26
|
-
conversation: [{ body: 'The CSV export returns an error.' }],
|
|
27
|
-
evidence: [{ url: 'https://evidence.invalid/screenshot' }],
|
|
28
|
-
});
|
|
29
|
-
await expectNoA11yViolations(container);
|
|
30
|
-
});
|
|
31
|
-
it('renders projected board work and selects a request', async () => {
|
|
32
|
-
const onselect = vi.fn();
|
|
33
|
-
const { container } = render(DevelopmentBoard, {
|
|
34
|
-
props: {
|
|
35
|
-
requests: [
|
|
36
|
-
{
|
|
37
|
-
id: 'r1',
|
|
38
|
-
description: 'Add CSV export',
|
|
39
|
-
type: 'feature',
|
|
40
|
-
status: 'planned',
|
|
41
|
-
},
|
|
42
|
-
],
|
|
43
|
-
onselect,
|
|
44
|
-
},
|
|
45
|
-
});
|
|
46
|
-
await userEvent.click(screen.getByRole('button', { name: /Add CSV export/i }));
|
|
47
|
-
expect(onselect).toHaveBeenCalledWith(expect.objectContaining({ id: 'r1' }));
|
|
48
|
-
await expectNoA11yViolations(container);
|
|
49
|
-
});
|
|
50
|
-
it('renders an accessible empty state and provider-neutral issue detail', async () => {
|
|
51
|
-
const { container, rerender } = render(DevelopmentBoard);
|
|
52
|
-
expect(screen.getByText('No visible development work')).toBeVisible();
|
|
53
|
-
await expectNoA11yViolations(container);
|
|
54
|
-
await rerender({ requests: [] });
|
|
55
|
-
const detail = render(DevelopmentRequestDetail, {
|
|
56
|
-
props: {
|
|
57
|
-
request: {
|
|
58
|
-
id: 'r2',
|
|
59
|
-
title: 'Export invoices',
|
|
60
|
-
description: 'Add an invoice export.',
|
|
61
|
-
type: 'feature',
|
|
62
|
-
status: 'planned',
|
|
63
|
-
visibility: 'workspace',
|
|
64
|
-
requesterLabel: 'Client owner',
|
|
65
|
-
},
|
|
66
|
-
events: [
|
|
67
|
-
{
|
|
68
|
-
id: 'e1',
|
|
69
|
-
sequence: 1,
|
|
70
|
-
type: 'pull_request',
|
|
71
|
-
label: 'Pull request opened',
|
|
72
|
-
occurredAt: '2026-07-11T12:00:00Z',
|
|
73
|
-
},
|
|
74
|
-
],
|
|
75
|
-
},
|
|
76
|
-
});
|
|
77
|
-
expect(screen.getByRole('heading', { name: 'Export invoices' })).toBeVisible();
|
|
78
|
-
expect(screen.getByText('Pull request opened')).toBeVisible();
|
|
79
|
-
await expectNoA11yViolations(detail.container);
|
|
80
|
-
});
|
|
81
|
-
it('allows one preview decision and hides actions after completion', async () => {
|
|
82
|
-
const ondecide = vi.fn();
|
|
83
|
-
const { container, rerender } = render(PreviewApprovalPanel, {
|
|
84
|
-
props: {
|
|
85
|
-
preview: { id: 'a1', previewId: 'pv1', status: 'pending' },
|
|
86
|
-
ondecide,
|
|
87
|
-
},
|
|
88
|
-
});
|
|
89
|
-
await userEvent.click(screen.getByRole('button', { name: 'Approve preview' }));
|
|
90
|
-
expect(ondecide).toHaveBeenCalledWith(true);
|
|
91
|
-
await rerender({
|
|
92
|
-
preview: { id: 'a1', previewId: 'pv1', status: 'approved' },
|
|
93
|
-
ondecide,
|
|
94
|
-
});
|
|
95
|
-
expect(screen.queryByRole('button', { name: 'Approve preview' })).not.toBeInTheDocument();
|
|
96
|
-
await expectNoA11yViolations(container);
|
|
97
|
-
});
|
|
98
|
-
});
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
import { expectNoA11yViolations, render, screen, userEvent, } from '@happyvertical/smrt-vitest/svelte';
|
|
3
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
-
import DevelopmentRequestForm from '../DevelopmentRequestForm.svelte';
|
|
5
|
-
import DevelopmentRequestList from '../DevelopmentRequestList.svelte';
|
|
6
|
-
describe('development request components', () => {
|
|
7
|
-
it('submits a request through the browser-safe callback surface', async () => {
|
|
8
|
-
const onsubmit = vi.fn();
|
|
9
|
-
render(DevelopmentRequestForm, { props: { onsubmit } });
|
|
10
|
-
await userEvent.selectOptions(screen.getByLabelText('Request type'), 'bug');
|
|
11
|
-
await userEvent.type(screen.getByLabelText('Description'), 'The save button is broken');
|
|
12
|
-
await userEvent.click(screen.getByRole('button', { name: 'Submit request' }));
|
|
13
|
-
expect(onsubmit).toHaveBeenCalledWith(expect.objectContaining({
|
|
14
|
-
type: 'bug',
|
|
15
|
-
description: 'The save button is broken',
|
|
16
|
-
visibility: 'requester',
|
|
17
|
-
origin: 'managed-app',
|
|
18
|
-
}));
|
|
19
|
-
});
|
|
20
|
-
it('renders requester-scoped status without exposing another requester', () => {
|
|
21
|
-
render(DevelopmentRequestList, {
|
|
22
|
-
props: {
|
|
23
|
-
requesterId: 'user-1',
|
|
24
|
-
requests: [
|
|
25
|
-
{
|
|
26
|
-
id: 'r1',
|
|
27
|
-
requesterId: 'user-1',
|
|
28
|
-
type: 'feature',
|
|
29
|
-
description: 'Dark mode',
|
|
30
|
-
status: 'planned',
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
id: 'r2',
|
|
34
|
-
requesterId: 'user-2',
|
|
35
|
-
type: 'bug',
|
|
36
|
-
description: 'Private bug',
|
|
37
|
-
status: 'submitted',
|
|
38
|
-
},
|
|
39
|
-
],
|
|
40
|
-
},
|
|
41
|
-
});
|
|
42
|
-
expect(screen.getByText('Dark mode')).toBeInTheDocument();
|
|
43
|
-
expect(screen.getByText('planned')).toBeInTheDocument();
|
|
44
|
-
expect(screen.queryByText('Private bug')).not.toBeInTheDocument();
|
|
45
|
-
});
|
|
46
|
-
it('is accessible', async () => {
|
|
47
|
-
const form = render(DevelopmentRequestForm, {
|
|
48
|
-
props: { onsubmit: vi.fn() },
|
|
49
|
-
});
|
|
50
|
-
await expectNoA11yViolations(form.container);
|
|
51
|
-
});
|
|
52
|
-
});
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
/**
|
|
3
|
-
* First component test in smrt-projects via the shared S11 harness (#1416).
|
|
4
|
-
*/
|
|
5
|
-
import { expectNoA11yViolations, render, screen, } from '@happyvertical/smrt-vitest/svelte';
|
|
6
|
-
import { describe, expect, it } from 'vitest';
|
|
7
|
-
import DurationDisplay from '../DurationDisplay.svelte';
|
|
8
|
-
describe('DurationDisplay', () => {
|
|
9
|
-
it('renders decimal hours to one decimal place', () => {
|
|
10
|
-
render(DurationDisplay, { props: { hours: 2.5 } });
|
|
11
|
-
expect(screen.getByText('2.5')).toBeInTheDocument();
|
|
12
|
-
});
|
|
13
|
-
it('renders an HH:MM value in hhmm format', () => {
|
|
14
|
-
render(DurationDisplay, { props: { hours: 1.5, format: 'hhmm' } });
|
|
15
|
-
expect(screen.getByText('1:30')).toBeInTheDocument();
|
|
16
|
-
});
|
|
17
|
-
// #2605: `showLabel` used to be inert. `decimal` rendered `h` whatever the
|
|
18
|
-
// prop said, and `hhmm` rendered an empty `<span class="unit">` instead of a
|
|
19
|
-
// label. These pin the unit to the prop, and pin the unchanged defaults so a
|
|
20
|
-
// later change to them has to be deliberate.
|
|
21
|
-
describe('showLabel', () => {
|
|
22
|
-
it('renders the decimal unit by default', () => {
|
|
23
|
-
const { container } = render(DurationDisplay, { props: { hours: 2.5 } });
|
|
24
|
-
expect(container.querySelector('.unit')?.textContent).toBe('h');
|
|
25
|
-
});
|
|
26
|
-
it('renders the decimal unit when showLabel is set', () => {
|
|
27
|
-
const { container } = render(DurationDisplay, {
|
|
28
|
-
props: { hours: 2.5, showLabel: true },
|
|
29
|
-
});
|
|
30
|
-
expect(container.querySelector('.unit')?.textContent).toBe('h');
|
|
31
|
-
});
|
|
32
|
-
it('drops the decimal unit when showLabel is cleared', () => {
|
|
33
|
-
const { container } = render(DurationDisplay, {
|
|
34
|
-
props: { hours: 2.5, showLabel: false },
|
|
35
|
-
});
|
|
36
|
-
expect(container.querySelector('.unit')).toBeNull();
|
|
37
|
-
expect(container.textContent?.trim()).toBe('2.5');
|
|
38
|
-
});
|
|
39
|
-
it.each([
|
|
40
|
-
undefined,
|
|
41
|
-
true,
|
|
42
|
-
false,
|
|
43
|
-
])('renders no unit element in hhmm format with showLabel=%s', (showLabel) => {
|
|
44
|
-
const { container } = render(DurationDisplay, {
|
|
45
|
-
props: { hours: 1.5, format: 'hhmm', showLabel },
|
|
46
|
-
});
|
|
47
|
-
expect(container.querySelector('.unit')).toBeNull();
|
|
48
|
-
expect(container.textContent?.trim()).toBe('1:30');
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
it('is axe-clean', async () => {
|
|
52
|
-
const { container } = render(DurationDisplay, {
|
|
53
|
-
props: { hours: 8, showLabel: true },
|
|
54
|
-
});
|
|
55
|
-
await expectNoA11yViolations(container);
|
|
56
|
-
});
|
|
57
|
-
});
|
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
import { expectNoA11yViolations, fireEvent, render, screen, userEvent, } from '@happyvertical/smrt-vitest/svelte';
|
|
3
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
-
import ProjectBoard from '../../ProjectBoard.svelte';
|
|
5
|
-
const statuses = [
|
|
6
|
-
{ id: 'todo', name: 'Todo', order: 0 },
|
|
7
|
-
{ id: 'done', name: 'Done', order: 1 },
|
|
8
|
-
];
|
|
9
|
-
const items = [
|
|
10
|
-
{
|
|
11
|
-
id: 'item-1',
|
|
12
|
-
contentId: 'content-1',
|
|
13
|
-
title: 'Publish release notes',
|
|
14
|
-
status: 'Todo',
|
|
15
|
-
fields: {},
|
|
16
|
-
type: 'Issue',
|
|
17
|
-
},
|
|
18
|
-
{
|
|
19
|
-
id: 'item-2',
|
|
20
|
-
contentId: 'content-2',
|
|
21
|
-
title: 'Stale provider status',
|
|
22
|
-
status: 'Provider-only',
|
|
23
|
-
fields: {},
|
|
24
|
-
type: 'DraftIssue',
|
|
25
|
-
},
|
|
26
|
-
];
|
|
27
|
-
const sameStatusItems = [
|
|
28
|
-
items[0],
|
|
29
|
-
{
|
|
30
|
-
id: 'item-3',
|
|
31
|
-
contentId: 'content-3',
|
|
32
|
-
title: 'Review release notes',
|
|
33
|
-
status: 'Todo',
|
|
34
|
-
fields: {},
|
|
35
|
-
type: 'Issue',
|
|
36
|
-
},
|
|
37
|
-
];
|
|
38
|
-
describe('ProjectBoard', () => {
|
|
39
|
-
it('does not persist same-status keyboard or pointer reordering', async () => {
|
|
40
|
-
const onmove = vi.fn().mockResolvedValue(undefined);
|
|
41
|
-
const onrefresh = vi.fn().mockResolvedValue(undefined);
|
|
42
|
-
render(ProjectBoard, {
|
|
43
|
-
props: {
|
|
44
|
-
projectId: 'project-1',
|
|
45
|
-
statuses,
|
|
46
|
-
items: sameStatusItems,
|
|
47
|
-
onmove,
|
|
48
|
-
onrefresh,
|
|
49
|
-
},
|
|
50
|
-
});
|
|
51
|
-
const first = screen.getByRole('button', {
|
|
52
|
-
name: /Publish release notes/,
|
|
53
|
-
});
|
|
54
|
-
const second = screen.getByRole('button', {
|
|
55
|
-
name: /Review release notes/,
|
|
56
|
-
});
|
|
57
|
-
first.focus();
|
|
58
|
-
await userEvent.keyboard(' ');
|
|
59
|
-
await userEvent.keyboard('{ArrowDown}');
|
|
60
|
-
await userEvent.keyboard(' ');
|
|
61
|
-
fireEvent.dragStart(first, { dataTransfer: { setData: vi.fn() } });
|
|
62
|
-
fireEvent.drop(second, { clientY: 999 });
|
|
63
|
-
expect(onmove).not.toHaveBeenCalled();
|
|
64
|
-
expect(onrefresh).not.toHaveBeenCalled();
|
|
65
|
-
expect(screen.queryByText(/Moved Publish release notes to Todo/i)).not.toBeInTheDocument();
|
|
66
|
-
});
|
|
67
|
-
it('stays read-only when a browser move callback has no authoritative refresh', async () => {
|
|
68
|
-
const onmove = vi.fn();
|
|
69
|
-
render(ProjectBoard, {
|
|
70
|
-
// Runtime callers can bypass the public discriminated callback contract.
|
|
71
|
-
// The adapter must still avoid a move that cannot reconcile its cards.
|
|
72
|
-
props: {
|
|
73
|
-
projectId: 'project-1',
|
|
74
|
-
statuses,
|
|
75
|
-
items: [items[0]],
|
|
76
|
-
onmove,
|
|
77
|
-
},
|
|
78
|
-
});
|
|
79
|
-
const card = screen.getByRole('button', {
|
|
80
|
-
name: /Publish release notes/,
|
|
81
|
-
});
|
|
82
|
-
await userEvent.click(card);
|
|
83
|
-
card.focus();
|
|
84
|
-
await userEvent.keyboard(' ');
|
|
85
|
-
await userEvent.keyboard('{ArrowRight}');
|
|
86
|
-
await userEvent.keyboard(' ');
|
|
87
|
-
expect(onmove).not.toHaveBeenCalled();
|
|
88
|
-
});
|
|
89
|
-
it('preserves supplied status order, keeps unmatched items visible, and sends a pure move intent', async () => {
|
|
90
|
-
const onmove = vi.fn().mockResolvedValue(undefined);
|
|
91
|
-
const onrefresh = vi.fn().mockResolvedValue(undefined);
|
|
92
|
-
const onselect = vi.fn();
|
|
93
|
-
const { container } = render(ProjectBoard, {
|
|
94
|
-
props: {
|
|
95
|
-
projectId: 'project-1',
|
|
96
|
-
statuses,
|
|
97
|
-
items,
|
|
98
|
-
onmove,
|
|
99
|
-
onrefresh,
|
|
100
|
-
onselect,
|
|
101
|
-
},
|
|
102
|
-
});
|
|
103
|
-
const lanes = Array.from(container.querySelectorAll('.smrt-board__lane'));
|
|
104
|
-
expect(lanes.map((lane) => lane.getAttribute('aria-label'))).toEqual([
|
|
105
|
-
'Todo, 1 cards',
|
|
106
|
-
'Done, 0 cards',
|
|
107
|
-
'Unassigned, 1 cards',
|
|
108
|
-
]);
|
|
109
|
-
expect(screen.getByText('Stale provider status')).toBeVisible();
|
|
110
|
-
const card = screen.getByRole('button', {
|
|
111
|
-
name: /Publish release notes/,
|
|
112
|
-
});
|
|
113
|
-
await userEvent.click(card);
|
|
114
|
-
expect(onselect).toHaveBeenCalledWith(items[0]);
|
|
115
|
-
card.focus();
|
|
116
|
-
await userEvent.keyboard(' ');
|
|
117
|
-
await userEvent.keyboard('{ArrowRight}');
|
|
118
|
-
await userEvent.keyboard(' ');
|
|
119
|
-
await vi.waitFor(() => {
|
|
120
|
-
expect(onmove).toHaveBeenCalledWith({
|
|
121
|
-
projectId: 'project-1',
|
|
122
|
-
itemId: 'item-1',
|
|
123
|
-
status: 'Done',
|
|
124
|
-
});
|
|
125
|
-
expect(onrefresh).toHaveBeenCalledTimes(1);
|
|
126
|
-
});
|
|
127
|
-
await expectNoA11yViolations(container);
|
|
128
|
-
});
|
|
129
|
-
it('refreshes authoritative state and lets Board make one sanitized failure announcement', async () => {
|
|
130
|
-
const onmove = vi
|
|
131
|
-
.fn()
|
|
132
|
-
.mockRejectedValue(new Error('provider token detail'));
|
|
133
|
-
const onrefresh = vi.fn().mockResolvedValue(undefined);
|
|
134
|
-
render(ProjectBoard, {
|
|
135
|
-
props: {
|
|
136
|
-
projectId: 'project-1',
|
|
137
|
-
statuses,
|
|
138
|
-
items: [items[0]],
|
|
139
|
-
onmove,
|
|
140
|
-
onrefresh,
|
|
141
|
-
},
|
|
142
|
-
});
|
|
143
|
-
const card = screen.getByRole('button', {
|
|
144
|
-
name: /Publish release notes/,
|
|
145
|
-
});
|
|
146
|
-
card.focus();
|
|
147
|
-
await userEvent.keyboard(' ');
|
|
148
|
-
await userEvent.keyboard('{ArrowRight}');
|
|
149
|
-
await userEvent.keyboard(' ');
|
|
150
|
-
await vi.waitFor(() => {
|
|
151
|
-
expect(onrefresh).toHaveBeenCalledTimes(1);
|
|
152
|
-
expect(screen.getByText('Could not move Publish release notes. The board was restored.')).toBeVisible();
|
|
153
|
-
expect(screen.getAllByText('Could not move Publish release notes. The board was restored.')).toHaveLength(1);
|
|
154
|
-
});
|
|
155
|
-
});
|
|
156
|
-
});
|