@eturnity/eturnity_reusable_components 9.25.10 → 9.25.12

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 CHANGED
@@ -212,3 +212,16 @@ Example:
212
212
  ```bash
213
213
  npm run promote:provided-files-tag -- --version=9.15.2
214
214
  ```
215
+
216
+ ## Testing
217
+
218
+ Unit tests are Jest + `@vue/test-utils`, co-located with each component as
219
+ `componentName.spec.js`. Run a single spec with `pnpm test <path>` or the full
220
+ suite with `pnpm test`. See [`docs/component-testing.md`](docs/component-testing.md)
221
+ for how we write tests (conventions, the TDD workflow, and using Claude Code).
222
+
223
+ ## CI and pull requests
224
+
225
+ Opening or updating a pull request runs **Bitbucket Pipelines** ([`bitbucket-pipelines.yml`](bitbucket-pipelines.yml)): install with pnpm, **`pnpm test`**, then **`pnpm run build`**. PRs are expected to pass this pipeline.
226
+
227
+ **Blocking merges on failure** is configured in **Bitbucket repository settings** (not in this repo): enable merge checks that require a successful pull-request build—for example a minimum number of successful builds for the PR, or “require passing pipelines” if your plan supports it. Until that is enabled, a red pipeline is visible but may not stop merges automatically.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eturnity/eturnity_reusable_components",
3
- "version": "9.25.10",
3
+ "version": "9.25.12",
4
4
  "files": [
5
5
  "dist",
6
6
  "src"
@@ -25,8 +25,7 @@
25
25
  "test-coverage": "jest --coverage",
26
26
  "merge-remote-master": "node scripts/merge-remote-master.js",
27
27
  "prepack": "npm run verify:provided-files && npm run verify:provided-docs",
28
- "maintain:components": "command -v agent >/dev/null 2>&1 && agent -p \"Run the UpdateTestsAndStories skill on all .vue files in src/components/\" --trust || echo '⚠️ Cursor CLI (agent) not found — skipping maintenance (run manually in Cursor)'",
29
- "prepublishOnly": "npm run maintain:components && npm run test && npm run build && npm run verify:provided-files && npm run verify:provided-docs"
28
+ "prepublishOnly": "npm run test && npm run build && npm run verify:provided-files && npm run verify:provided-docs"
30
29
  },
31
30
  "peerDependencies": {
32
31
  "date-fns": "^3.0.0 || ^4.0.0",
@@ -0,0 +1,34 @@
1
+ /* eslint-disable */
2
+ import { mount } from '@vue/test-utils'
3
+ import CloseButton from '@/components/buttons/closeButton'
4
+
5
+ // Canonical example spec for the reusable component library.
6
+ // Reference for: mount, data-id / data-qa-id selectors, structural assertions.
7
+ // `theme` and `$gettext` are provided globally by jest.setup.js, so specs do
8
+ // not need to re-provide them.
9
+
10
+ describe('CloseButton', () => {
11
+ const mountButton = (props = {}) => mount(CloseButton, { props })
12
+
13
+ it('renders the two crossing lines inside the container', () => {
14
+ const container = mountButton({ dataId: 'close_btn' }).find(
15
+ '[data-id="close_btn"]'
16
+ )
17
+ expect(container.exists()).toBe(true)
18
+ expect(container.findAll('div')).toHaveLength(2)
19
+ })
20
+
21
+ it('exposes dataId and dataQaId as data attributes', () => {
22
+ const wrapper = mountButton({ dataId: 'close_btn', dataQaId: 'qa_close' })
23
+ expect(wrapper.find('[data-id="close_btn"]').exists()).toBe(true)
24
+ expect(wrapper.find('[data-qa-id="qa_close"]').exists()).toBe(true)
25
+ })
26
+
27
+ it('applies a generated style class to each line', () => {
28
+ const lines = mountButton({ dataId: 'close_btn' })
29
+ .find('[data-id="close_btn"]')
30
+ .findAll('div')
31
+ expect(lines).toHaveLength(2)
32
+ lines.forEach((line) => expect(line.classes().length).toBeGreaterThan(0))
33
+ })
34
+ })
@@ -70,7 +70,7 @@
70
70
  },
71
71
  })
72
72
 
73
- const emit = defineEmits(['update:modelValue'])
73
+ const emit = defineEmits(['update:modelValue', 'click'])
74
74
 
75
75
  const getButtonPosition = computed(() => (index) => {
76
76
  if (index === 0) return 'first'
@@ -0,0 +1,40 @@
1
+ /* eslint-disable */
2
+ import { mount } from '@vue/test-utils'
3
+ import SplitButtons from '@/components/buttons/splitButtons'
4
+
5
+ describe('SplitButtons', () => {
6
+ const options = [
7
+ { label: 'Monthly', value: 'monthly' },
8
+ { label: 'Yearly', value: 'yearly' },
9
+ ]
10
+
11
+ const mountButtons = (props = {}) =>
12
+ mount(SplitButtons, {
13
+ props: { options, modelValue: 'monthly', dataIdKey: 'billing', ...props },
14
+ })
15
+
16
+ it('renders one button per option with its label', () => {
17
+ const buttons = mountButtons().findAll('button')
18
+ expect(buttons).toHaveLength(2)
19
+ expect(buttons[0].text()).toBe('Monthly')
20
+ expect(buttons[1].text()).toBe('Yearly')
21
+ })
22
+
23
+ it('builds the data-id-key from the key and the option value', () => {
24
+ const wrapper = mountButtons()
25
+ expect(wrapper.find('[data-id-key="billing_monthly"]').exists()).toBe(true)
26
+ expect(wrapper.find('[data-id-key="billing_yearly"]').exists()).toBe(true)
27
+ })
28
+
29
+ it('emits update:modelValue with the clicked option value', async () => {
30
+ const wrapper = mountButtons()
31
+ await wrapper.findAll('button')[1].trigger('click')
32
+ expect(wrapper.emitted('update:modelValue')).toEqual([['yearly']])
33
+ })
34
+
35
+ it('emits a declared click event with the clicked option value', async () => {
36
+ const wrapper = mountButtons()
37
+ await wrapper.findAll('button')[1].trigger('click')
38
+ expect(wrapper.emitted('click')).toEqual([['yearly']])
39
+ })
40
+ })
@@ -1,34 +1,34 @@
1
+ /* eslint-disable */
1
2
  import { mount } from '@vue/test-utils'
2
3
  import ErrorMessage from '@/components/errorMessage'
3
- import theme from '@/assets/theme'
4
-
5
- /* eslint-disable */
6
4
 
7
- describe('ErrorMessage Component', () => {
8
- let wrapper
9
-
10
- beforeEach(() => {
11
- wrapper = mount(ErrorMessage, {
12
- props: {},
5
+ describe('ErrorMessage', () => {
6
+ const mountMessage = (props = {}) =>
7
+ mount(ErrorMessage, {
8
+ props,
13
9
  slots: {
14
10
  default: '<div data-test-id="fake-msg">testing</div>',
15
11
  },
16
- global: {
17
- provide: {
18
- theme,
19
- },
20
- },
21
12
  })
13
+
14
+ it('renders the default slot content', () => {
15
+ const wrapper = mountMessage()
16
+ const message = wrapper.find('[data-test-id="fake-msg"]')
17
+ expect(message.exists()).toBe(true)
18
+ expect(message.text()).toBe('testing')
22
19
  })
23
20
 
24
- test('renders ErrorMessage component with default props', () => {
25
- expect(wrapper.findAll('[data-test-id="fake-msg"]').length).toBe(1)
21
+ it('uses the documented default props', () => {
22
+ const wrapper = mountMessage()
23
+ expect(wrapper.props('isRelative')).toBe(false)
24
+ expect(wrapper.props('marginTop')).toBe('13px')
26
25
  })
27
26
 
28
- test('applies the correct CSS class for styling', async () => {
29
- wrapper.setProps({ marginTop: '20px' })
27
+ it('renders different overlay styling for absolute (default) vs relative', () => {
28
+ const overlayClassOf = (props) =>
29
+ mountMessage(props).find('[data-test-id="fake-msg"]').element
30
+ .parentElement.className
30
31
 
31
- await wrapper.vm.$nextTick()
32
- expect(wrapper.props().marginTop).toBe('20px')
32
+ expect(overlayClassOf()).not.toBe(overlayClassOf({ isRelative: true }))
33
33
  })
34
34
  })
@@ -125,8 +125,8 @@
125
125
  props.theme.colors[props.labelFontColor]
126
126
  ? props.theme.colors[props.labelFontColor]
127
127
  : props.labelFontColor
128
- ? props.labelFontColor
129
- : props.theme.colors.eturnityGrey};
128
+ ? props.labelFontColor
129
+ : props.theme.colors.eturnityGrey};
130
130
 
131
131
  font-size: ${(props) =>
132
132
  props.fontSize ? props.fontSize : inputLabelFontSize};
@@ -172,29 +172,29 @@
172
172
  props.noBorder
173
173
  ? 'none'
174
174
  : props.isError
175
- ? '1px solid ' + props.theme.colors.red
176
- : props.borderColor
177
- ? props.theme.colors[props.borderColor]
178
- ? '1px solid ' + props.theme.colors[props.borderColor]
179
- : '1px solid ' + props.borderColor
180
- : '1px solid ' + props.theme.colors.grey4};
175
+ ? '1px solid ' + props.theme.colors.red
176
+ : props.borderColor
177
+ ? props.theme.colors[props.borderColor]
178
+ ? '1px solid ' + props.theme.colors[props.borderColor]
179
+ : '1px solid ' + props.borderColor
180
+ : '1px solid ' + props.theme.colors.grey4};
181
181
  padding: ${(props) =>
182
182
  props.isError
183
183
  ? '11px 25px 11px 10px'
184
184
  : props.inputType === 'password'
185
- ? '11px 25px 11px 10px'
186
- : props.defaultPadding
187
- ? '10px 35px 10px 15px'
188
- : '11px 5px 11px 10px'};
185
+ ? '11px 25px 11px 10px'
186
+ : props.defaultPadding
187
+ ? '10px 35px 10px 15px'
188
+ : '11px 5px 11px 10px'};
189
189
  border-radius: 4px;
190
190
  position: relative;
191
- font-size: ${(props) => (props.fontSize ? props.fontSize : '14px')};
191
+ font-size: ${(props) => (props.fontSize ? props.fontSize : '13px')};
192
192
  color: ${(props) =>
193
193
  props.isDisabled
194
194
  ? props.theme.colors.grey2
195
195
  : props.fontColor
196
- ? props.fontColor + ' !important'
197
- : props.theme.semanticColors.grey[900]};
196
+ ? props.fontColor + ' !important'
197
+ : props.theme.semanticColors.grey[900]};
198
198
 
199
199
  width: ${(props) => (props.inputWidth ? props.inputWidth : '100%')};
200
200
  min-width: ${(props) => (props.minWidth ? props.minWidth : 'unset')};
@@ -208,8 +208,8 @@
208
208
  ? props.disabledBackgroundColor + ' !important'
209
209
  : props.theme.colors.grey5
210
210
  : props.backgroundColor
211
- ? props.backgroundColor + ' !important'
212
- : props.theme.colors.white};
211
+ ? props.backgroundColor + ' !important'
212
+ : props.theme.colors.white};
213
213
  &::placeholder {
214
214
  color: ${(props) => props.theme.colors.grey2};
215
215
  }
@@ -3,8 +3,8 @@
3
3
  <PillWrapper
4
4
  v-if="markerData && markerData.translations[activeLanguage]"
5
5
  :cursor="editionAllowed ? 'pointer' : cursor"
6
- :with-date="!!date"
7
6
  :truncate-label="truncateLabel"
7
+ :with-date="!!date"
8
8
  @click="editionAllowed ? (activated = !activated) : null"
9
9
  >
10
10
  <PillContent :truncate-label="truncateLabel">
@@ -58,19 +58,23 @@
58
58
  :prevent-outside-close="true"
59
59
  @on-close="closeDeleteModal"
60
60
  >
61
- <ModalContainer>
62
- <PageTitle :text="$gettext('delete_confirm_text')" />
63
- <PageSubtitle :text="$gettext('delete_confirm_subtext')" />
64
- <CtaContainer>
65
- <MainButton :text="$gettext('Delete')" @click="onDelete" />
61
+ <ModalContent>
62
+ <ModalTitle>
63
+ {{ $gettext('delete_confirm_text') }}
64
+ </ModalTitle>
65
+ <ModalBody>
66
+ {{ $gettext('delete_confirm_subtext') }}
67
+ </ModalBody>
68
+ <ModalButtonContainer>
66
69
  <MainButton
67
70
  :text="$gettext('Cancel')"
68
- type="primary"
71
+ type="tertiary"
69
72
  variant="cancel"
70
73
  @click="closeDeleteModal"
71
74
  />
72
- </CtaContainer>
73
- </ModalContainer>
75
+ <MainButton :text="$gettext('Delete')" @click="onDelete" />
76
+ </ModalButtonContainer>
77
+ </ModalContent>
74
78
  </Modal>
75
79
  </PageContainer>
76
80
  </template>
@@ -94,9 +98,11 @@
94
98
  import vClickOutside from 'click-outside-vue3'
95
99
  import Icon from '../icon'
96
100
  import Modal from '../modals/modal'
97
- import PageTitle from '../pageTitle'
101
+ import ModalContent from '../modals/modalContent'
102
+ import ModalTitle from '../modals/modalTitle'
103
+ import ModalBody from '../modals/modalBody'
104
+ import ModalButtonContainer from '../modals/modalButtonContainer'
98
105
  import DeleteIcon from '../deleteIcon'
99
- import PageSubtitle from '../pageSubtitle'
100
106
  import MainButton from '../buttons/mainButton'
101
107
 
102
108
  const PageContainerProps = { truncateLabel: Boolean }
@@ -113,17 +119,6 @@
113
119
  `}
114
120
  `
115
121
 
116
- const ModalContainer = styled.div`
117
- padding: 40px;
118
- min-width: 500px;
119
- `
120
-
121
- const CtaContainer = styled.div`
122
- display: flex;
123
- gap: 20px;
124
- margin-top: 30px;
125
- `
126
-
127
122
  const PillWrapperProps = {
128
123
  withDate: Boolean,
129
124
  cursor: String,
@@ -281,10 +276,10 @@
281
276
  IconContainer,
282
277
  Icon,
283
278
  Modal,
284
- ModalContainer,
285
- CtaContainer,
286
- PageTitle,
287
- PageSubtitle,
279
+ ModalButtonContainer,
280
+ ModalTitle,
281
+ ModalBody,
282
+ ModalContent,
288
283
  MainButton,
289
284
  PillDate,
290
285
  MarkerName,