@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@citizenplane/pimp",
3
- "version": "18.23.3",
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
+ }
@@ -10,5 +10,14 @@
10
10
  "skipLibCheck": true
11
11
  },
12
12
  "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "src/**/*.d.ts"],
13
- "exclude": ["node_modules", "dist", "src/stories", "**/*.stories.ts", "**/*.stories.vue", "**/*.stories.tsx", "tests"]
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
  }
@@ -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
+ })