@citizenplane/pimp 18.23.3 → 18.23.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.
- package/dist/components/CpBasket.vue.d.ts +13 -2
- package/dist/components/CpBasket.vue.d.ts.map +1 -1
- package/dist/pimp.es.js +2 -2
- package/dist/pimp.umd.js +1 -1
- package/dist/style.css +1 -1
- package/package.json +3 -1
- package/src/components/CpBasket.spec.ts +90 -0
- package/src/components/CpBasket.vue +7 -2
- package/src/components/__snapshots__/CpBasket.spec.ts.snap +40 -0
- package/src/stories/CpBasket.stories.ts +53 -0
- package/tsconfig.build.json +10 -1
- package/vitest.config.js +32 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@citizenplane/pimp",
|
|
3
|
-
"version": "18.23.
|
|
3
|
+
"version": "18.23.4",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "storybook dev -p 8081",
|
|
6
6
|
"build-storybook": "storybook build --output-dir ./docs",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"lint:style:fix": "stylelint \"**/*.{css,scss,vue}\" --fix",
|
|
12
12
|
"format": "biome format --write .",
|
|
13
13
|
"types": "vue-tsc --noEmit",
|
|
14
|
+
"test": "vitest run",
|
|
14
15
|
"commitlint": "commitlint --edit",
|
|
15
16
|
"prepare": "husky"
|
|
16
17
|
},
|
|
@@ -74,6 +75,7 @@
|
|
|
74
75
|
"@vitest/coverage-v8": "4.1.9",
|
|
75
76
|
"@vue/test-utils": "2.4.11",
|
|
76
77
|
"commitlint-config-gitmoji": "2.3.1",
|
|
78
|
+
"happy-dom": "20.14.5",
|
|
77
79
|
"husky": "9.1.7",
|
|
78
80
|
"lint-staged": "17.2.0",
|
|
79
81
|
"playwright": "1.62.0",
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { mount } from '@vue/test-utils'
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import { defineComponent } from 'vue'
|
|
4
|
+
|
|
5
|
+
import type { CpBasketRow } from '@/constants/CpBasketTypes'
|
|
6
|
+
|
|
7
|
+
import CpBasket from '@/components/CpBasket.vue'
|
|
8
|
+
|
|
9
|
+
const seatsCategory = { id: 'seats', label: 'Seats', subtotal: '24,00 €' }
|
|
10
|
+
const promoCategory = { id: 'promo', label: 'Promo code', subtotal: '-10,00 €' }
|
|
11
|
+
|
|
12
|
+
const outboundGroup = { id: 'CDG-ORY', label: 'CDG → ORY' }
|
|
13
|
+
|
|
14
|
+
const textOnlyRows: CpBasketRow[] = [
|
|
15
|
+
{ label: 'Seat 12A', value: '12,00 €', category: seatsCategory, group: outboundGroup },
|
|
16
|
+
{ label: 'Carbon offset', value: '3,00 €', category: seatsCategory },
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
const promoRows: CpBasketRow[] = [{ label: 'Promo SUMMER25 applied', value: '-10,00 €', category: promoCategory }]
|
|
20
|
+
|
|
21
|
+
const defaultProps = { total: '47,00 €', totalLabel: 'Total' }
|
|
22
|
+
|
|
23
|
+
describe('CpBasket', () => {
|
|
24
|
+
it('renders text-only rows as two plain spans', () => {
|
|
25
|
+
const wrapper = mount(CpBasket, { props: { ...defaultProps, rows: textOnlyRows } })
|
|
26
|
+
|
|
27
|
+
const renderedRows = wrapper.findAll('.cpBasket__row')
|
|
28
|
+
|
|
29
|
+
expect(renderedRows).toHaveLength(2)
|
|
30
|
+
expect(renderedRows[0].element.innerHTML).toBe('<span>Seat 12A</span><span>12,00 €</span>')
|
|
31
|
+
expect(renderedRows[1].element.innerHTML).toBe('<span>Carbon offset</span><span>3,00 €</span>')
|
|
32
|
+
expect(wrapper.html()).toMatchSnapshot()
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('keeps the row markup untouched when only one cell slot is provided', () => {
|
|
36
|
+
const wrapper = mount(CpBasket, {
|
|
37
|
+
props: { ...defaultProps, rows: promoRows },
|
|
38
|
+
slots: { rowValue: '<span class="promoValue">{{ params.row.value }}</span>' },
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const renderedRow = wrapper.get('.cpBasket__row')
|
|
42
|
+
|
|
43
|
+
expect(renderedRow.element.children).toHaveLength(2)
|
|
44
|
+
expect(renderedRow.element.children[0].outerHTML).toBe('<span>Promo SUMMER25 applied</span>')
|
|
45
|
+
expect(renderedRow.get('.promoValue').text()).toBe('-10,00 €')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('reaches the consumer handler when a button in the label slot is clicked', async () => {
|
|
49
|
+
const removePromoCode = vi.fn()
|
|
50
|
+
|
|
51
|
+
const host = defineComponent({
|
|
52
|
+
components: { CpBasket },
|
|
53
|
+
setup: () => ({ defaultProps, promoRows, removePromoCode }),
|
|
54
|
+
template: `
|
|
55
|
+
<CpBasket v-bind="defaultProps" :rows="promoRows">
|
|
56
|
+
<template #rowLabel="{ row }">
|
|
57
|
+
<span>{{ row.label }} <button type="button" @click="removePromoCode">Remove</button></span>
|
|
58
|
+
</template>
|
|
59
|
+
</CpBasket>
|
|
60
|
+
`,
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const wrapper = mount(host)
|
|
64
|
+
|
|
65
|
+
const removeButton = wrapper.get('.cpBasket__row button')
|
|
66
|
+
|
|
67
|
+
expect(removeButton.element.tagName).toBe('BUTTON')
|
|
68
|
+
expect(removeButton.element.closest('.cpBasket__row')).toBe(wrapper.get('.cpBasket__row').element)
|
|
69
|
+
|
|
70
|
+
await removeButton.trigger('click')
|
|
71
|
+
|
|
72
|
+
expect(removePromoCode).toHaveBeenCalledTimes(1)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('right-aligns the value cell filled by the rowValue slot', () => {
|
|
76
|
+
const wrapper = mount(CpBasket, {
|
|
77
|
+
props: { ...defaultProps, rows: promoRows },
|
|
78
|
+
slots: { rowValue: '<span class="promoValue">{{ params.row.value }}</span>' },
|
|
79
|
+
attachTo: document.body,
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
const valueCell = wrapper.get('.promoValue').element
|
|
83
|
+
const valueCellStyle = window.getComputedStyle(valueCell)
|
|
84
|
+
|
|
85
|
+
expect(valueCellStyle.textAlign).toBe('right')
|
|
86
|
+
expect(valueCellStyle.fontVariantNumeric).toBe('tabular-nums')
|
|
87
|
+
|
|
88
|
+
wrapper.unmount()
|
|
89
|
+
})
|
|
90
|
+
})
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
<p v-if="group.label" class="cpBasket__route">{{ group.label }}</p>
|
|
19
19
|
<cp-transition-list-items>
|
|
20
20
|
<div v-for="row in group.rows" :key="row.key" :class="group.rowClass">
|
|
21
|
-
<span>{{ row.label }}</span>
|
|
22
|
-
<span>{{ row.value }}</span>
|
|
21
|
+
<slot name="rowLabel" :row="row"><span>{{ row.label }}</span></slot>
|
|
22
|
+
<slot name="rowValue" :row="row"><span>{{ row.value }}</span></slot>
|
|
23
23
|
</div>
|
|
24
24
|
</cp-transition-list-items>
|
|
25
25
|
</div>
|
|
@@ -243,6 +243,11 @@ const hasSections = computed(() => visibleCategories.value.length > 0)
|
|
|
243
243
|
&--isIndented {
|
|
244
244
|
padding-left: var(--cp-spacing-md);
|
|
245
245
|
}
|
|
246
|
+
|
|
247
|
+
& > :last-child {
|
|
248
|
+
font-variant-numeric: tabular-nums;
|
|
249
|
+
text-align: right;
|
|
250
|
+
}
|
|
246
251
|
}
|
|
247
252
|
|
|
248
253
|
&__total {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`CpBasket > renders text-only rows as two plain spans 1`] = `
|
|
4
|
+
"<div class="cpBasket">
|
|
5
|
+
<!--v-if-->
|
|
6
|
+
<transition-stub data-v-19d824c7="" name="expand" appear="false" persisted="false" css="true">
|
|
7
|
+
<div>
|
|
8
|
+
<div class="cpBasket__details">
|
|
9
|
+
<div class="cpBasket__category">
|
|
10
|
+
<div class="cpBasket__categoryTitle">
|
|
11
|
+
<p>Seats</p>
|
|
12
|
+
<p>24,00 €</p>
|
|
13
|
+
</div>
|
|
14
|
+
<div class="cpBasket__groups">
|
|
15
|
+
<transition-group-stub name="list-items" appear="false" persisted="false" css="true">
|
|
16
|
+
<div class="cpBasket__group">
|
|
17
|
+
<p class="cpBasket__route">CDG → ORY</p>
|
|
18
|
+
<transition-group-stub name="list-items" appear="false" persisted="false" css="true">
|
|
19
|
+
<div class="cpBasket__row cpBasket__row--isIndented"><span>Seat 12A</span><span>12,00 €</span></div>
|
|
20
|
+
</transition-group-stub>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="cpBasket__group">
|
|
23
|
+
<!--v-if-->
|
|
24
|
+
<transition-group-stub name="list-items" appear="false" persisted="false" css="true">
|
|
25
|
+
<div class="cpBasket__row"><span>Carbon offset</span><span>3,00 €</span></div>
|
|
26
|
+
</transition-group-stub>
|
|
27
|
+
</div>
|
|
28
|
+
</transition-group-stub>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
</transition-stub>
|
|
34
|
+
<div class="cpBasket__total">
|
|
35
|
+
<p>Total</p>
|
|
36
|
+
<p class="cpBasket__totalValue">47,00 €</p>
|
|
37
|
+
</div>
|
|
38
|
+
<!--v-if-->
|
|
39
|
+
</div>"
|
|
40
|
+
`;
|
|
@@ -99,6 +99,14 @@ const fakeRowPool: CpBasketRow[] = [
|
|
|
99
99
|
{ label: 'Priority boarding', value: '8,00 €', category: extrasCategory, group: null },
|
|
100
100
|
]
|
|
101
101
|
|
|
102
|
+
const promoCategory = { id: 'promo', label: 'Promo code', subtotal: '-10,00 €' }
|
|
103
|
+
|
|
104
|
+
const promoRows: CpBasketRow[] = [
|
|
105
|
+
{ label: 'Seat 12A', value: '12,00 €', category: seatsCategory, group: outboundGroup },
|
|
106
|
+
{ label: '2 × Cabin baggage 10kg', value: '20,00 €', category: baggageCategory, group: outboundGroup },
|
|
107
|
+
{ label: 'Promo SUMMER25 applied', value: '-10,00 €', category: promoCategory },
|
|
108
|
+
]
|
|
109
|
+
|
|
102
110
|
const flightsCategory = { id: 'flights', label: 'Flights', subtotal: '159,80 €' }
|
|
103
111
|
const hiddenSeatsCategory = { id: 'seats', label: 'Seats', subtotal: '99,60 €' }
|
|
104
112
|
const tripExtrasCategory = { id: 'tripExtras', label: 'Trip extras', subtotal: '269,40 €' }
|
|
@@ -321,3 +329,48 @@ export const TrailingSlot: Story = {
|
|
|
321
329
|
`,
|
|
322
330
|
}),
|
|
323
331
|
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* `#rowLabel` and `#rowValue` let the consumer own one cell of a row while the
|
|
335
|
+
* rest of the basket keeps its default text rendering. Here the promo row
|
|
336
|
+
* carries a real `cp-button` next to its label and a success-toned negative
|
|
337
|
+
* amount, and clicking "Remove" drops the row from the list.
|
|
338
|
+
*/
|
|
339
|
+
export const PromoCodeRow: Story = {
|
|
340
|
+
args: {
|
|
341
|
+
rows: promoRows,
|
|
342
|
+
total: '22,00 €',
|
|
343
|
+
totalLabel: 'Total',
|
|
344
|
+
},
|
|
345
|
+
render: (args: Args) => ({
|
|
346
|
+
components: { CpBasket, CpButton },
|
|
347
|
+
setup() {
|
|
348
|
+
const storyRows = ref<CpBasketRow[]>([...promoRows])
|
|
349
|
+
|
|
350
|
+
const removePromoCode = () => {
|
|
351
|
+
storyRows.value = storyRows.value.filter((row) => row.category.id !== promoCategory.id)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return { args, promoCategoryId: promoCategory.id, removePromoCode, storyRows }
|
|
355
|
+
},
|
|
356
|
+
template: `
|
|
357
|
+
<div style="max-width: 415px;">
|
|
358
|
+
<CpBasket v-bind="args" :rows="storyRows">
|
|
359
|
+
<template #rowLabel="{ row }">
|
|
360
|
+
<span v-if="row.category.id === promoCategoryId">
|
|
361
|
+
{{ row.label }}
|
|
362
|
+
<CpButton appearance="tertiary" color="accent" size="2xs" @click="removePromoCode">Remove</CpButton>
|
|
363
|
+
</span>
|
|
364
|
+
<span v-else>{{ row.label }}</span>
|
|
365
|
+
</template>
|
|
366
|
+
<template #rowValue="{ row }">
|
|
367
|
+
<span v-if="row.category.id === promoCategoryId" style="color: var(--cp-text-success-primary);">
|
|
368
|
+
{{ row.value }}
|
|
369
|
+
</span>
|
|
370
|
+
<span v-else>{{ row.value }}</span>
|
|
371
|
+
</template>
|
|
372
|
+
</CpBasket>
|
|
373
|
+
</div>
|
|
374
|
+
`,
|
|
375
|
+
}),
|
|
376
|
+
}
|
package/tsconfig.build.json
CHANGED
|
@@ -10,5 +10,14 @@
|
|
|
10
10
|
"skipLibCheck": true
|
|
11
11
|
},
|
|
12
12
|
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "src/**/*.d.ts"],
|
|
13
|
-
"exclude": [
|
|
13
|
+
"exclude": [
|
|
14
|
+
"node_modules",
|
|
15
|
+
"dist",
|
|
16
|
+
"src/stories",
|
|
17
|
+
"**/*.spec.ts",
|
|
18
|
+
"**/*.stories.ts",
|
|
19
|
+
"**/*.stories.vue",
|
|
20
|
+
"**/*.stories.tsx",
|
|
21
|
+
"tests"
|
|
22
|
+
]
|
|
14
23
|
}
|
package/vitest.config.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import vue from '@vitejs/plugin-vue'
|
|
4
|
+
import { defineConfig } from 'vitest/config'
|
|
5
|
+
|
|
6
|
+
const dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
7
|
+
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
plugins: [vue()],
|
|
10
|
+
resolve: {
|
|
11
|
+
alias: {
|
|
12
|
+
'@': path.resolve(dirname, 'src'),
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
css: {
|
|
16
|
+
preprocessorOptions: {
|
|
17
|
+
scss: {
|
|
18
|
+
additionalData: `
|
|
19
|
+
@use "sass:math";
|
|
20
|
+
@use '@/assets/styles/helpers/functions' as fn;
|
|
21
|
+
@use '@/assets/styles/helpers/mixins' as mx;
|
|
22
|
+
@use '@/assets/styles/utilities';`,
|
|
23
|
+
api: 'modern-compiler',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
test: {
|
|
28
|
+
css: true,
|
|
29
|
+
environment: 'happy-dom',
|
|
30
|
+
include: ['src/**/*.spec.ts'],
|
|
31
|
+
},
|
|
32
|
+
})
|