@retalia/pos-components 0.0.3 → 0.0.4

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.
Files changed (45) hide show
  1. package/eslint.config.js +32 -0
  2. package/ng-package.json +14 -0
  3. package/package.json +17 -31
  4. package/src/lib/basket/pos-basket.html +124 -0
  5. package/src/lib/basket/pos-basket.scss +285 -0
  6. package/src/lib/basket/pos-basket.spec.ts +198 -0
  7. package/src/lib/basket/pos-basket.stories.ts +114 -0
  8. package/src/lib/basket/pos-basket.ts +142 -0
  9. package/src/lib/item-entry/pos-item-entry.html +41 -0
  10. package/src/lib/item-entry/pos-item-entry.scss +145 -0
  11. package/src/lib/item-entry/pos-item-entry.spec.ts +123 -0
  12. package/src/lib/item-entry/pos-item-entry.stories.ts +31 -0
  13. package/src/lib/item-entry/pos-item-entry.ts +81 -0
  14. package/src/lib/login/pos-login.html +82 -0
  15. package/src/lib/login/pos-login.scss +238 -0
  16. package/src/lib/login/pos-login.spec.ts +89 -0
  17. package/src/lib/login/pos-login.stories.ts +36 -0
  18. package/src/lib/login/pos-login.ts +150 -0
  19. package/src/lib/receipt/pos-receipt.html +116 -0
  20. package/src/lib/receipt/pos-receipt.scss +367 -0
  21. package/src/lib/receipt/pos-receipt.spec.ts +188 -0
  22. package/src/lib/receipt/pos-receipt.stories.ts +163 -0
  23. package/src/lib/receipt/pos-receipt.ts +129 -0
  24. package/src/lib/tender/pos-tender.html +79 -0
  25. package/src/lib/tender/pos-tender.scss +318 -0
  26. package/src/lib/tender/pos-tender.spec.ts +150 -0
  27. package/src/lib/tender/pos-tender.stories.ts +47 -0
  28. package/src/lib/tender/pos-tender.ts +154 -0
  29. package/src/lib/tokens/pos-theme-css.spec.ts +75 -0
  30. package/src/lib/tokens/pos-theme-css.ts +123 -0
  31. package/src/lib/tokens/pos-token-catalog.ts +514 -0
  32. package/src/lib/tokens/pos-tokens.html +351 -0
  33. package/src/lib/tokens/pos-tokens.scss +437 -0
  34. package/src/lib/tokens/pos-tokens.spec.ts +164 -0
  35. package/src/lib/tokens/pos-tokens.stories.ts +22 -0
  36. package/src/lib/tokens/pos-tokens.ts +208 -0
  37. package/src/public-api.ts +37 -0
  38. package/tsconfig.lib.json +14 -0
  39. package/tsconfig.lib.prod.json +11 -0
  40. package/tsconfig.spec.json +11 -0
  41. package/fesm2022/retalia-pos-components.mjs +0 -1089
  42. package/fesm2022/retalia-pos-components.mjs.map +0 -1
  43. package/types/retalia-pos-components.d.ts +0 -300
  44. /package/{styles → src/styles}/styles.css +0 -0
  45. /package/{styles → src/styles}/tokens.css +0 -0
@@ -0,0 +1,32 @@
1
+ // @ts-check
2
+ const { defineConfig } = require('eslint/config');
3
+ const rootConfig = require('../../eslint.config.js');
4
+
5
+ module.exports = defineConfig([
6
+ ...rootConfig,
7
+ {
8
+ files: ['**/*.ts'],
9
+ rules: {
10
+ '@angular-eslint/directive-selector': [
11
+ 'error',
12
+ {
13
+ type: 'attribute',
14
+ prefix: 'pos',
15
+ style: 'camelCase',
16
+ },
17
+ ],
18
+ '@angular-eslint/component-selector': [
19
+ 'error',
20
+ {
21
+ type: 'element',
22
+ prefix: 'pos',
23
+ style: 'kebab-case',
24
+ },
25
+ ],
26
+ },
27
+ },
28
+ {
29
+ files: ['**/*.html'],
30
+ rules: {},
31
+ },
32
+ ]);
@@ -0,0 +1,14 @@
1
+ {
2
+ "$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
3
+ "dest": "../../dist/pos-components",
4
+ "lib": {
5
+ "entryFile": "src/public-api.ts"
6
+ },
7
+ "assets": [
8
+ {
9
+ "glob": "**/*",
10
+ "input": "src/styles",
11
+ "output": "styles"
12
+ }
13
+ ]
14
+ }
package/package.json CHANGED
@@ -1,31 +1,17 @@
1
- {
2
- "name": "@retalia/pos-components",
3
- "version": "0.0.3",
4
- "publishConfig": {
5
- "access": "public",
6
- "registry": "https://registry.npmjs.org/"
7
- },
8
- "peerDependencies": {
9
- "@angular/common": "^21.2.0",
10
- "@angular/core": "^21.2.0"
11
- },
12
- "dependencies": {
13
- "tslib": "^2.3.0"
14
- },
15
- "sideEffects": [
16
- "**/*.css"
17
- ],
18
- "style": "./styles/styles.css",
19
- "module": "fesm2022/retalia-pos-components.mjs",
20
- "typings": "types/retalia-pos-components.d.ts",
21
- "exports": {
22
- "./package.json": {
23
- "default": "./package.json"
24
- },
25
- ".": {
26
- "types": "./types/retalia-pos-components.d.ts",
27
- "default": "./fesm2022/retalia-pos-components.mjs"
28
- }
29
- },
30
- "type": "module"
31
- }
1
+ {
2
+ "name": "@retalia/pos-components",
3
+ "version": "0.0.4",
4
+ "publishConfig": {
5
+ "access": "public",
6
+ "registry": "https://registry.npmjs.org/"
7
+ },
8
+ "peerDependencies": {
9
+ "@angular/common": "^21.2.0",
10
+ "@angular/core": "^21.2.0"
11
+ },
12
+ "dependencies": {
13
+ "tslib": "^2.3.0"
14
+ },
15
+ "sideEffects": ["**/*.css"],
16
+ "style": "./styles/styles.css"
17
+ }
@@ -0,0 +1,124 @@
1
+ <section
2
+ class="pos-basket__panel"
3
+ [attr.aria-busy]="disabled() ? true : null"
4
+ >
5
+ <header class="pos-basket__header">
6
+ <h2 class="pos-basket__title">{{ title() }}</h2>
7
+ <p class="pos-basket__count">
8
+ {{ lines().length }}
9
+ {{ lines().length === 1 ? 'line' : 'lines' }}
10
+ </p>
11
+ </header>
12
+
13
+ <ul class="pos-basket__lines" aria-label="Basket lines">
14
+ @if (!hasLines()) {
15
+ <li class="pos-basket__empty">{{ emptyMessage() }}</li>
16
+ } @else {
17
+ @for (line of lines(); track line.lineId) {
18
+ <li>
19
+ <article
20
+ class="pos-basket__line"
21
+ [class.pos-basket__line--voided]="line.struckThrough"
22
+ >
23
+ <div class="pos-basket__line-info">
24
+ <h3 class="pos-basket__line-name">{{ line.name }}</h3>
25
+ <p class="pos-basket__line-sku">{{ line.sku }}</p>
26
+ @if (line.struckThrough) {
27
+ <p class="pos-basket__line-voided">Voided</p>
28
+ }
29
+ </div>
30
+
31
+ <div
32
+ class="pos-basket__stepper"
33
+ role="group"
34
+ [attr.aria-label]="'Quantity for ' + line.name"
35
+ >
36
+ <button
37
+ type="button"
38
+ class="pos-basket__qty-btn"
39
+ aria-label="Decrease quantity"
40
+ [disabled]="!canDecrement(line)"
41
+ (click)="onDecrement(line)"
42
+ >
43
+
44
+ </button>
45
+ <span class="pos-basket__qty" aria-live="polite">{{ line.qty }}</span>
46
+ <button
47
+ type="button"
48
+ class="pos-basket__qty-btn"
49
+ aria-label="Increase quantity"
50
+ [disabled]="!canChangeQuantity(line)"
51
+ (click)="onIncrement(line)"
52
+ >
53
+ +
54
+ </button>
55
+ </div>
56
+
57
+ <div class="pos-basket__line-prices">
58
+ <p class="pos-basket__unit-price">{{ formatMoney(line.unitPrice) }}</p>
59
+ <p class="pos-basket__line-total">{{ formatMoney(line.lineTotal) }}</p>
60
+ </div>
61
+
62
+ <button
63
+ type="button"
64
+ class="pos-basket__void"
65
+ [attr.aria-label]="'Void ' + line.name"
66
+ [disabled]="!canVoid(line)"
67
+ (click)="onVoid(line)"
68
+ >
69
+ <svg
70
+ xmlns="http://www.w3.org/2000/svg"
71
+ width="18"
72
+ height="18"
73
+ viewBox="0 0 24 24"
74
+ fill="none"
75
+ aria-hidden="true"
76
+ >
77
+ <path
78
+ d="M6 6l12 12M18 6 6 18"
79
+ stroke="currentColor"
80
+ stroke-width="2"
81
+ stroke-linecap="round"
82
+ />
83
+ </svg>
84
+ </button>
85
+ </article>
86
+ </li>
87
+ }
88
+ }
89
+ </ul>
90
+
91
+ <footer class="pos-basket__footer">
92
+ <dl class="pos-basket__totals" aria-live="polite">
93
+ <div class="pos-basket__total-row">
94
+ <dt>Gross</dt>
95
+ <dd>{{ formatMoney(totals().gross) }}</dd>
96
+ </div>
97
+ <div class="pos-basket__total-row">
98
+ <dt>Net</dt>
99
+ <dd>{{ formatMoney(totals().net) }}</dd>
100
+ </div>
101
+ <div class="pos-basket__total-row">
102
+ <dt>VAT</dt>
103
+ <dd>{{ formatMoney(totals().vat) }}</dd>
104
+ </div>
105
+ <div class="pos-basket__total-row">
106
+ <dt>Discount</dt>
107
+ <dd>{{ formatMoney(totals().discount) }}</dd>
108
+ </div>
109
+ <div class="pos-basket__total-row pos-basket__total-row--due">
110
+ <dt>Due</dt>
111
+ <dd>{{ formatMoney(totals().due) }}</dd>
112
+ </div>
113
+ </dl>
114
+
115
+ <button
116
+ type="button"
117
+ class="pos-basket__clear"
118
+ [disabled]="!canClear()"
119
+ (click)="onClear()"
120
+ >
121
+ {{ clearLabel() }}
122
+ </button>
123
+ </footer>
124
+ </section>
@@ -0,0 +1,285 @@
1
+ :host {
2
+ display: flex;
3
+ flex-direction: column;
4
+ min-height: 0;
5
+ width: 100%;
6
+ height: 100%;
7
+ color: var(--pos-color-text);
8
+ font-family: var(--pos-font-family);
9
+ font-size: var(--pos-font-size-md);
10
+ }
11
+
12
+ .pos-basket__panel {
13
+ display: flex;
14
+ flex-direction: column;
15
+ min-height: 0;
16
+ height: 100%;
17
+ padding: var(--pos-space-lg);
18
+ border: 1px solid var(--pos-color-border);
19
+ border-radius: var(--pos-radius-lg);
20
+ background: var(--pos-color-panel);
21
+ }
22
+
23
+ .pos-basket__header {
24
+ display: flex;
25
+ align-items: baseline;
26
+ justify-content: space-between;
27
+ gap: var(--pos-space-md);
28
+ margin-bottom: var(--pos-space-md);
29
+ }
30
+
31
+ .pos-basket__title {
32
+ margin: 0;
33
+ font-family: var(--pos-font-family-display);
34
+ font-size: var(--pos-font-size-xl);
35
+ font-weight: var(--pos-font-weight-bold);
36
+ letter-spacing: -0.03em;
37
+ }
38
+
39
+ .pos-basket__count {
40
+ margin: 0;
41
+ color: var(--pos-color-text-muted);
42
+ font-size: var(--pos-font-size-sm);
43
+ font-weight: var(--pos-font-weight-semibold);
44
+ }
45
+
46
+ .pos-basket__lines {
47
+ display: flex;
48
+ flex: 1 1 auto;
49
+ flex-direction: column;
50
+ gap: var(--pos-space-sm);
51
+ min-height: 8rem;
52
+ margin: 0;
53
+ padding: 0;
54
+ overflow-y: auto;
55
+ list-style: none;
56
+ }
57
+
58
+ .pos-basket__empty {
59
+ margin: auto 0;
60
+ padding: var(--pos-space-2xl) var(--pos-space-md);
61
+ color: var(--pos-color-text-muted);
62
+ text-align: center;
63
+ }
64
+
65
+ .pos-basket__line {
66
+ display: grid;
67
+ grid-template-columns: minmax(0, 1fr) auto;
68
+ grid-template-areas:
69
+ 'info void'
70
+ 'stepper prices';
71
+ align-items: center;
72
+ gap: var(--pos-space-sm) var(--pos-space-md);
73
+ padding: var(--pos-space-md);
74
+ border: 1px solid var(--pos-color-border);
75
+ border-radius: var(--pos-radius-md);
76
+ background: var(--pos-color-panel-elevated);
77
+ }
78
+
79
+ .pos-basket__line-info {
80
+ grid-area: info;
81
+ min-width: 0;
82
+ }
83
+
84
+ .pos-basket__line-name {
85
+ margin: 0;
86
+ font-size: var(--pos-font-size-md);
87
+ font-weight: var(--pos-font-weight-semibold);
88
+ overflow: hidden;
89
+ text-overflow: ellipsis;
90
+ white-space: nowrap;
91
+ }
92
+
93
+ .pos-basket__line-sku,
94
+ .pos-basket__line-voided {
95
+ margin: var(--pos-space-xs) 0 0;
96
+ font-size: var(--pos-font-size-sm);
97
+ }
98
+
99
+ .pos-basket__line-sku {
100
+ color: var(--pos-color-text-muted);
101
+ }
102
+
103
+ .pos-basket__line-voided {
104
+ color: var(--pos-color-danger);
105
+ font-weight: var(--pos-font-weight-semibold);
106
+ }
107
+
108
+ .pos-basket__stepper {
109
+ grid-area: stepper;
110
+ display: inline-flex;
111
+ align-items: center;
112
+ gap: var(--pos-space-xs);
113
+ width: fit-content;
114
+ padding: var(--pos-space-xs);
115
+ border: 1px solid var(--pos-color-border);
116
+ border-radius: var(--pos-radius-md);
117
+ background: var(--pos-color-surface);
118
+ }
119
+
120
+ .pos-basket__qty-btn,
121
+ .pos-basket__void {
122
+ display: inline-flex;
123
+ align-items: center;
124
+ justify-content: center;
125
+ margin: 0;
126
+ border: 0;
127
+ background: transparent;
128
+ color: var(--pos-color-text);
129
+ font: inherit;
130
+ cursor: pointer;
131
+ -webkit-tap-highlight-color: transparent;
132
+ }
133
+
134
+ .pos-basket__qty-btn {
135
+ min-width: 2.75rem;
136
+ min-height: 2.75rem;
137
+ border-radius: var(--pos-radius-sm);
138
+ font-size: var(--pos-font-size-lg);
139
+ font-weight: var(--pos-font-weight-bold);
140
+ }
141
+
142
+ .pos-basket__qty-btn:not(:disabled):active,
143
+ .pos-basket__void:not(:disabled):active {
144
+ background: var(--pos-color-key-active);
145
+ color: var(--pos-color-on-key-active);
146
+ }
147
+
148
+ .pos-basket__qty {
149
+ min-width: 1.75rem;
150
+ font-family: var(--pos-font-family-display);
151
+ font-weight: var(--pos-font-weight-bold);
152
+ text-align: center;
153
+ }
154
+
155
+ .pos-basket__line-prices {
156
+ grid-area: prices;
157
+ display: flex;
158
+ flex-direction: column;
159
+ align-items: flex-end;
160
+ gap: var(--pos-space-xs);
161
+ }
162
+
163
+ .pos-basket__unit-price {
164
+ margin: 0;
165
+ color: var(--pos-color-text-muted);
166
+ font-size: var(--pos-font-size-sm);
167
+ }
168
+
169
+ .pos-basket__line-total {
170
+ margin: 0;
171
+ font-family: var(--pos-font-family-display);
172
+ font-weight: var(--pos-font-weight-bold);
173
+ }
174
+
175
+ .pos-basket__void {
176
+ grid-area: void;
177
+ justify-self: end;
178
+ min-width: 2.75rem;
179
+ min-height: 2.75rem;
180
+ border: 1px solid var(--pos-color-border);
181
+ border-radius: var(--pos-radius-md);
182
+ color: var(--pos-color-text-muted);
183
+ }
184
+
185
+ .pos-basket__qty-btn:disabled,
186
+ .pos-basket__void:disabled,
187
+ .pos-basket__clear:disabled {
188
+ cursor: not-allowed;
189
+ opacity: 0.55;
190
+ }
191
+
192
+ .pos-basket__qty-btn:focus-visible,
193
+ .pos-basket__void:focus-visible,
194
+ .pos-basket__clear:focus-visible {
195
+ outline: 2px solid var(--pos-color-primary);
196
+ outline-offset: 2px;
197
+ }
198
+
199
+ .pos-basket__line--voided .pos-basket__line-name,
200
+ .pos-basket__line--voided .pos-basket__line-sku,
201
+ .pos-basket__line--voided .pos-basket__line-prices {
202
+ color: var(--pos-color-text-muted);
203
+ text-decoration: line-through;
204
+ }
205
+
206
+ .pos-basket__footer {
207
+ display: flex;
208
+ flex-direction: column;
209
+ gap: var(--pos-space-md);
210
+ margin-top: var(--pos-space-md);
211
+ padding-top: var(--pos-space-md);
212
+ border-top: 1px solid var(--pos-color-border);
213
+ }
214
+
215
+ .pos-basket__totals {
216
+ display: flex;
217
+ flex-direction: column;
218
+ gap: var(--pos-space-sm);
219
+ margin: 0;
220
+ }
221
+
222
+ .pos-basket__total-row {
223
+ display: flex;
224
+ justify-content: space-between;
225
+ gap: var(--pos-space-md);
226
+ margin: 0;
227
+ color: var(--pos-color-text-muted);
228
+ font-size: var(--pos-font-size-sm);
229
+ }
230
+
231
+ .pos-basket__total-row dt,
232
+ .pos-basket__total-row dd {
233
+ margin: 0;
234
+ }
235
+
236
+ .pos-basket__total-row--due {
237
+ padding-top: var(--pos-space-sm);
238
+ border-top: 1px solid var(--pos-color-border);
239
+ color: var(--pos-color-text);
240
+ font-family: var(--pos-font-family-display);
241
+ font-size: var(--pos-font-size-lg);
242
+ font-weight: var(--pos-font-weight-bold);
243
+ }
244
+
245
+ .pos-basket__clear {
246
+ min-height: var(--pos-button-min-height);
247
+ border: 0;
248
+ border-radius: var(--pos-radius-md);
249
+ background: var(--pos-color-danger);
250
+ color: var(--pos-color-on-danger);
251
+ font: inherit;
252
+ font-weight: var(--pos-font-weight-bold);
253
+ letter-spacing: 0.04em;
254
+ text-transform: uppercase;
255
+ cursor: pointer;
256
+ -webkit-tap-highlight-color: transparent;
257
+ }
258
+
259
+ .pos-basket__clear:not(:disabled):active {
260
+ transform: translateY(1px);
261
+ }
262
+
263
+ .pos-basket__clear:disabled {
264
+ background: var(--pos-color-disabled);
265
+ color: var(--pos-color-on-disabled);
266
+ transform: none;
267
+ }
268
+
269
+ @media (min-width: 768px) {
270
+ .pos-basket__panel {
271
+ padding: var(--pos-space-xl);
272
+ }
273
+
274
+ .pos-basket__line {
275
+ grid-template-columns: minmax(0, 1fr) auto auto auto;
276
+ grid-template-areas: 'info stepper prices void';
277
+ gap: var(--pos-space-md);
278
+ }
279
+ }
280
+
281
+ @media (prefers-reduced-motion: reduce) {
282
+ .pos-basket__clear {
283
+ transform: none;
284
+ }
285
+ }
@@ -0,0 +1,198 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+
3
+ import {
4
+ PosBasket,
5
+ PosBasketIntent,
6
+ PosBasketLine,
7
+ PosBasketTotals,
8
+ } from './pos-basket';
9
+
10
+ const COFFEE_LINE: PosBasketLine = {
11
+ lineId: 1,
12
+ sku: 'SKU-001',
13
+ name: 'Coffee 250g',
14
+ qty: 2,
15
+ unitPrice: 6,
16
+ lineTotal: 12,
17
+ vat: { rate: 0.25, amount: 2.4, included: true },
18
+ struckThrough: false,
19
+ };
20
+
21
+ const MID_SALE_TOTALS: PosBasketTotals = {
22
+ gross: 12,
23
+ net: 9.6,
24
+ vat: 2.4,
25
+ discount: 0,
26
+ due: 12,
27
+ currency: 'EUR',
28
+ };
29
+
30
+ describe('PosBasket', () => {
31
+ let fixture: ComponentFixture<PosBasket>;
32
+ let component: PosBasket;
33
+
34
+ beforeEach(async () => {
35
+ await TestBed.configureTestingModule({
36
+ imports: [PosBasket],
37
+ }).compileComponents();
38
+
39
+ fixture = TestBed.createComponent(PosBasket);
40
+ component = fixture.componentInstance;
41
+ await fixture.whenStable();
42
+ });
43
+
44
+ it('should render the empty basket state by default', () => {
45
+ const compiled = fixture.nativeElement as HTMLElement;
46
+ expect(compiled.querySelector('.pos-basket__title')?.textContent).toContain(
47
+ 'Basket',
48
+ );
49
+ expect(compiled.querySelector('.pos-basket__empty')?.textContent).toContain(
50
+ 'Basket is empty',
51
+ );
52
+ });
53
+
54
+ it('should render lines and totals exactly as received without calculating', async () => {
55
+ const mismatched: PosBasketLine = {
56
+ ...COFFEE_LINE,
57
+ qty: 2,
58
+ unitPrice: 6,
59
+ lineTotal: 99,
60
+ };
61
+ const totals: PosBasketTotals = {
62
+ ...MID_SALE_TOTALS,
63
+ gross: 50,
64
+ due: 40,
65
+ };
66
+
67
+ fixture.componentRef.setInput('lines', [mismatched]);
68
+ fixture.componentRef.setInput('totals', totals);
69
+ await fixture.whenStable();
70
+
71
+ const compiled = fixture.nativeElement as HTMLElement;
72
+ expect(
73
+ compiled.querySelector('.pos-basket__line-name')?.textContent,
74
+ ).toContain('Coffee 250g');
75
+ expect(
76
+ compiled.querySelector('.pos-basket__line-sku')?.textContent,
77
+ ).toContain('SKU-001');
78
+ expect(compiled.querySelector('.pos-basket__qty')?.textContent?.trim()).toBe(
79
+ '2',
80
+ );
81
+ expect(
82
+ compiled.querySelector('.pos-basket__line-total')?.textContent,
83
+ ).toContain('99');
84
+ expect(
85
+ compiled.querySelector('.pos-basket__line-total')?.textContent,
86
+ ).not.toContain('12');
87
+
88
+ const due = compiled.querySelector('.pos-basket__total-row--due dd');
89
+ expect(due?.textContent).toContain('40');
90
+ expect(due?.textContent).not.toContain('50');
91
+ });
92
+
93
+ it('should emit change-quantity when the cashier increments a line', async () => {
94
+ const intents: PosBasketIntent[] = [];
95
+ component.action.subscribe((intent) => intents.push(intent));
96
+
97
+ fixture.componentRef.setInput('lines', [COFFEE_LINE]);
98
+ fixture.componentRef.setInput('totals', MID_SALE_TOTALS);
99
+ await fixture.whenStable();
100
+
101
+ const increase = (
102
+ fixture.nativeElement as HTMLElement
103
+ ).querySelectorAll('.pos-basket__qty-btn')[1] as HTMLButtonElement;
104
+ increase.click();
105
+ await fixture.whenStable();
106
+
107
+ expect(intents).toEqual([
108
+ { type: 'basket.changeQuantity', lineId: 1, qty: 3 },
109
+ ]);
110
+ });
111
+
112
+ it('should emit change-quantity when the cashier decrements a line', async () => {
113
+ const intents: PosBasketIntent[] = [];
114
+ component.action.subscribe((intent) => intents.push(intent));
115
+
116
+ fixture.componentRef.setInput('lines', [COFFEE_LINE]);
117
+ fixture.componentRef.setInput('totals', MID_SALE_TOTALS);
118
+ await fixture.whenStable();
119
+
120
+ const decrease = (
121
+ fixture.nativeElement as HTMLElement
122
+ ).querySelector('.pos-basket__qty-btn') as HTMLButtonElement;
123
+ decrease.click();
124
+ await fixture.whenStable();
125
+
126
+ expect(intents).toEqual([
127
+ { type: 'basket.changeQuantity', lineId: 1, qty: 1 },
128
+ ]);
129
+ });
130
+
131
+ it('should emit void-line for the selected line', async () => {
132
+ const intents: PosBasketIntent[] = [];
133
+ component.action.subscribe((intent) => intents.push(intent));
134
+
135
+ fixture.componentRef.setInput('lines', [COFFEE_LINE]);
136
+ fixture.componentRef.setInput('totals', MID_SALE_TOTALS);
137
+ await fixture.whenStable();
138
+
139
+ const voidBtn = (fixture.nativeElement as HTMLElement).querySelector(
140
+ '.pos-basket__void',
141
+ ) as HTMLButtonElement;
142
+ voidBtn.click();
143
+ await fixture.whenStable();
144
+
145
+ expect(intents).toEqual([{ type: 'basket.voidLine', lineId: 1 }]);
146
+ });
147
+
148
+ it('should emit clear-basket when the cashier clears', async () => {
149
+ const intents: PosBasketIntent[] = [];
150
+ component.action.subscribe((intent) => intents.push(intent));
151
+
152
+ fixture.componentRef.setInput('lines', [COFFEE_LINE]);
153
+ fixture.componentRef.setInput('totals', MID_SALE_TOTALS);
154
+ await fixture.whenStable();
155
+
156
+ const clear = (fixture.nativeElement as HTMLElement).querySelector(
157
+ '.pos-basket__clear',
158
+ ) as HTMLButtonElement;
159
+ clear.click();
160
+ await fixture.whenStable();
161
+
162
+ expect(intents).toEqual([{ type: 'basket.clear' }]);
163
+ });
164
+
165
+ it('should not emit while disabled, and should disable void on struck-through lines', async () => {
166
+ const intents: PosBasketIntent[] = [];
167
+ component.action.subscribe((intent) => intents.push(intent));
168
+
169
+ fixture.componentRef.setInput('lines', [
170
+ { ...COFFEE_LINE, struckThrough: true },
171
+ ]);
172
+ fixture.componentRef.setInput('totals', MID_SALE_TOTALS);
173
+ fixture.componentRef.setInput('disabled', true);
174
+ await fixture.whenStable();
175
+
176
+ const compiled = fixture.nativeElement as HTMLElement;
177
+ const increase = compiled.querySelectorAll(
178
+ '.pos-basket__qty-btn',
179
+ )[1] as HTMLButtonElement;
180
+ const voidBtn = compiled.querySelector(
181
+ '.pos-basket__void',
182
+ ) as HTMLButtonElement;
183
+ const clear = compiled.querySelector(
184
+ '.pos-basket__clear',
185
+ ) as HTMLButtonElement;
186
+
187
+ increase.click();
188
+ voidBtn.click();
189
+ clear.click();
190
+ await fixture.whenStable();
191
+
192
+ expect(increase.disabled).toBe(true);
193
+ expect(voidBtn.disabled).toBe(true);
194
+ expect(clear.disabled).toBe(true);
195
+ expect(intents).toEqual([]);
196
+ expect(compiled.querySelector('.pos-basket__line--voided')).toBeTruthy();
197
+ });
198
+ });