glib-web 5.0.7 → 6.0.1

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 (58) hide show
  1. package/.claude/commands/gen-cypress-test.md +101 -0
  2. package/.github/dependabot.yml +24 -12
  3. package/.nycrc.json +4 -0
  4. package/AGENTS.md +1 -0
  5. package/README.md +4 -0
  6. package/actions/logics/set.js +5 -0
  7. package/actions/panels/scrollTo.js +2 -2
  8. package/actions/timeouts/set.js +1 -1
  9. package/app.scss +19 -0
  10. package/app.vue +1 -1
  11. package/components/charts/series.js +2 -2
  12. package/components/composable/gmap.js +1 -1
  13. package/components/composable/upload.js +1 -4
  14. package/components/composable/upload_nothing.js +1 -1
  15. package/components/fields/_buttonDate.vue +2 -2
  16. package/components/fields/_patternText.vue +2 -8
  17. package/components/fields/_select.vue +10 -8
  18. package/components/fields/_selectItemDefault.vue +1 -3
  19. package/components/fields/_selectItemWithIcon.vue +1 -3
  20. package/components/fields/_selectItemWithImage.vue +1 -3
  21. package/components/fields/clipboardUpload.vue +115 -0
  22. package/components/fields/dynamicGroup2.vue +1 -1
  23. package/components/fields/multiUpload.vue +12 -19
  24. package/components/fields/placeholderUpload.vue +8 -4
  25. package/components/fields/radioGroup.vue +0 -7
  26. package/components/fields/sign.vue +1 -1
  27. package/components/fields/text.vue +1 -9
  28. package/components/fields/upload.vue +4 -0
  29. package/components/mixins/longClick.js +1 -1
  30. package/components/panels/bulkEdit2.vue +1 -1
  31. package/components/panels/flow.vue +19 -10
  32. package/components/panels/list.vue +1 -1
  33. package/components/popover.vue +1 -1
  34. package/components/responsive.vue +1 -1
  35. package/cypress/e2e/glib-web/dialog.cy.js +0 -1
  36. package/cypress/e2e/glib-web/dirtyState.cy.js +37 -37
  37. package/cypress/e2e/glib-web/fieldsDateTime.cy.js +46 -3
  38. package/cypress/e2e/glib-web/fieldsRadio.cy.js +65 -0
  39. package/cypress/e2e/glib-web/fieldsSelect.cy.js +116 -76
  40. package/cypress/e2e/glib-web/fieldsText.cy.js +83 -0
  41. package/cypress/e2e/glib-web/fieldsUpload.cy.js +230 -120
  42. package/cypress/e2e/glib-web/image.cy.js +62 -33
  43. package/cypress/e2e/glib-web/switch.cy.js +13 -0
  44. package/cypress/e2e/glib-web/tabBar.cy.js +23 -0
  45. package/cypress/fixtures/document.pdf +12 -0
  46. package/cypress/fixtures/large.png +0 -0
  47. package/cypress/fixtures/upload.png +0 -0
  48. package/cypress/helper.js +13 -1
  49. package/cypress/support/component.js +1 -1
  50. package/cypress/support/e2e.js +20 -13
  51. package/doc/dependabot.md +22 -0
  52. package/eslint-rules/index.js +6 -6
  53. package/nav/dialog.vue +1 -1
  54. package/package.json +18 -16
  55. package/templates/_menu.vue +2 -2
  56. package/cypress/component/inputUpload.cy.js +0 -103
  57. package/cypress/component/multiUpload.cy.js +0 -107
  58. package/cypress/component/placeholderUpload.cy.js +0 -91
@@ -1,57 +1,30 @@
1
- import { testPageUrl } from "../../helper.js"
1
+ import { testPageUrl, withComponent, withComponentThen } from "../../helper.js";
2
2
 
3
- const url = testPageUrl('fields_upload')
4
-
5
- const getRealComponent = (win, component) => {
6
- const { isObject, isNotNull } = win.Utils.type
7
- let realComp = component
8
-
9
- while (isObject(realComp) && isNotNull(realComp.$refs?.delegate)) {
10
- realComp = realComp.$refs.delegate
11
- }
12
-
13
- return realComp
14
- }
3
+ const url = testPageUrl('fields_upload');
15
4
 
16
5
  describe('fields_upload', () => {
17
- it('invokes upload triggers and resets', () => {
18
- cy.visit(url)
19
-
20
- cy.window().then((win) => {
21
- const basicComp = getRealComponent(win, win.GLib.component.findById('fields_upload_basic'))
22
- const filesComp = getRealComponent(win, win.GLib.component.findById('fields_upload_files'))
23
-
24
- cy.wrap(basicComp).should('exist')
25
- cy.wrap(filesComp).should('exist')
26
6
 
27
- cy.spy(basicComp, 'trigger').as('basicTrigger')
28
- cy.spy(basicComp, 'reset').as('basicReset')
29
- cy.spy(filesComp, 'trigger').as('filesTrigger')
30
- })
7
+ it('resets the avatar placeholderView field value', () => {
8
+ cy.visit(url);
31
9
 
32
- cy.contains('Trigger').click()
33
- cy.get('@basicTrigger').should('have.been.calledOnce')
10
+ withComponent('fields_upload_avatar', (comp) => cy.spy(comp, 'reset').as('avatarReset'));
34
11
 
35
- cy.contains('Reset').click()
36
- cy.get('@basicReset').should('have.been.calledOnce')
12
+ cy.get('.v-btn').filter(':contains("Reset")').eq(1).click();
13
+ cy.get('@avatarReset').should('have.been.calledOnce');
37
14
 
38
- cy.contains('Select file').click()
39
- cy.get('@filesTrigger').should('have.been.calledOnce')
40
- })
15
+ cy.contains('submit').click();
16
+ cy.get('.unformatted').should('contain.text', 'Method: POST').and('not.contain.text', '"file_avatar": "test"');
17
+ cy.get('.v-dialog').contains('OK').click();
18
+ });
41
19
 
42
20
  it('handles multi progress view drag and drop uploads', () => {
43
- const htmlUploadUrl = 'http://localhost:3000/glib/json_ui_garage?path=forms%2Ffile_upload_new&mode=html'
44
- let uploadIndex = 0
45
- const pngBase64 =
46
- 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGNgYAAAAAMAASsJTYQAAAAASUVORK5CYII='
47
- const pngFile = {
48
- contents: Cypress.Buffer.from(pngBase64, 'base64'),
49
- fileName: 'upload.png',
50
- mimeType: 'image/png'
51
- }
21
+ const htmlUploadUrl = 'http://localhost:3000/glib/json_ui_garage?path=forms%2Ffile_upload_new&mode=html';
22
+ let uploadIndex = 0;
23
+
24
+ cy.fixture('upload.png', null).as('pngContents');
52
25
 
53
26
  cy.intercept('POST', /\/(glib\/glib_direct_uploads|rails\/active_storage\/direct_uploads)$/, (req) => {
54
- uploadIndex += 1
27
+ uploadIndex += 1;
55
28
  req.reply({
56
29
  statusCode: 200,
57
30
  body: {
@@ -63,97 +36,234 @@ describe('fields_upload', () => {
63
36
  }
64
37
  }
65
38
  }
66
- })
67
- }).as('directUploadCreate')
39
+ });
40
+ }).as('directUploadCreate');
68
41
 
69
42
  cy.intercept('PUT', 'https://example.com/rails/active_storage/blobs/upload', {
70
43
  statusCode: 200
71
- }).as('directUploadStore')
72
-
73
- cy.visit(url)
74
-
75
- cy.window()
76
- .should((win) => {
77
- const multiComp = getRealComponent(win, win.GLib.component.findById('fields_upload_multiprogress'))
78
- expect(multiComp, 'multi progress component').to.exist
79
- expect(multiComp.$el, 'multi progress element').to.exist
80
- expect(multiComp.$el.querySelector('.gdrop-file'), 'multi progress dropzone').to.exist
81
- })
82
- .then((win) => {
83
- const multiComp = getRealComponent(win, win.GLib.component.findById('fields_upload_multiprogress'))
84
- cy.wrap(multiComp.$el).as('multiRoot')
85
- cy.wrap(multiComp.$el.querySelector('.gdrop-file')).as('dropzone')
86
- })
87
-
88
- cy.get('@dropzone').should('have.class', 'border-[2px]')
89
- cy.get('@dropzone').trigger('drag')
90
- cy.get('@dropzone').trigger('dragover')
91
- cy.get('@dropzone').should('have.class', 'border-[4px]').and('not.have.class', 'border-[2px]')
92
- cy.get('@dropzone').trigger('dragleave')
93
- cy.get('@dropzone').should('have.class', 'border-[2px]')
94
-
95
- cy.get('@dropzone').click({ force: true })
96
- cy.get('@multiRoot').find('input[type="file"]').should('exist')
97
-
98
- cy.get('@dropzone').selectFile(
99
- pngFile,
100
- { action: 'drag-drop' }
101
- )
102
-
103
- cy.wait('@directUploadCreate')
104
- cy.wait('@directUploadStore')
44
+ }).as('directUploadStore');
45
+
46
+ cy.visit(url);
47
+
48
+ withComponentThen('fields_upload_multiprogress', (comp) => {
49
+ expect(comp.$el, 'multi progress element').to.exist;
50
+ expect(comp.$el.querySelector('.gdrop-file'), 'multi progress dropzone').to.exist;
51
+ cy.wrap(comp.$el).as('multiRoot');
52
+ cy.wrap(comp.$el.querySelector('.gdrop-file')).as('dropzone');
53
+ });
54
+
55
+ cy.get('@dropzone').should('have.class', 'border-[2px]');
56
+ cy.get('@dropzone').trigger('drag');
57
+ cy.get('@dropzone').trigger('dragover');
58
+ cy.get('@dropzone').should('have.class', 'border-[4px]').and('not.have.class', 'border-[2px]');
59
+ cy.get('@dropzone').trigger('dragleave');
60
+ cy.get('@dropzone').should('have.class', 'border-[2px]');
61
+
62
+ cy.get('@dropzone').click({ force: true });
63
+ cy.get('@multiRoot').find('input[type="file"]').should('exist');
64
+
65
+ cy.get('@pngContents').then((contents) => {
66
+ cy.get('@dropzone').selectFile(
67
+ { contents, fileName: 'upload.png', mimeType: 'image/png' },
68
+ { action: 'drag-drop' }
69
+ );
70
+ });
71
+
72
+ cy.wait('@directUploadCreate');
73
+ cy.wait('@directUploadStore');
105
74
  cy.get('@multiRoot')
106
75
  .find('input[type="hidden"][name="user[file_input_multiprogress][]"]')
107
- .should('have.length', 1)
76
+ .should('have.length', 1);
108
77
 
109
- cy.get('@dropzone').selectFile(
110
- { ...pngFile, fileName: 'second.png' },
111
- { action: 'drag-drop' }
112
- )
78
+ cy.get('@pngContents').then((contents) => {
79
+ cy.get('@dropzone').selectFile(
80
+ { contents, fileName: 'second.png', mimeType: 'image/png' },
81
+ { action: 'drag-drop' }
82
+ );
83
+ });
113
84
 
114
- cy.wait('@directUploadCreate')
115
- cy.wait('@directUploadStore')
85
+ cy.wait('@directUploadCreate');
86
+ cy.wait('@directUploadStore');
116
87
 
117
- cy.get('@multiRoot').find('.guploaded-file .file-container').should('have.length', 2)
88
+ cy.get('@multiRoot').find('.guploaded-file .file-container').should('have.length', 2);
118
89
  cy.get('@multiRoot')
119
90
  .find('input[type="hidden"][name="user[file_input_multiprogress][]"]')
120
- .should('have.length', 2)
121
- cy.get('@multiRoot').find('.guploaded-file .close-btn').first().click()
122
- cy.get('@multiRoot').find('.guploaded-file .file-container').should('have.length', 1)
91
+ .should('have.length', 2);
92
+ cy.get('@multiRoot').find('.guploaded-file .close-btn').first().click();
93
+ cy.get('@multiRoot').find('.guploaded-file .file-container').should('have.length', 1);
123
94
  cy.get('@multiRoot')
124
95
  .find('input[type="hidden"][name="user[file_input_multiprogress][]"]')
125
- .should('have.length', 1)
126
-
127
- cy.visit(htmlUploadUrl)
128
-
129
- cy.window()
130
- .should((win) => {
131
- const multiComp = getRealComponent(win, win.GLib.component.findById('mp1'))
132
- expect(multiComp, 'html multi progress component').to.exist
133
- expect(multiComp.$el, 'html multi progress element').to.exist
134
- expect(multiComp.$el.querySelector('.gdrop-file'), 'html multi progress dropzone').to.exist
135
- })
136
- .then((win) => {
137
- const multiComp = getRealComponent(win, win.GLib.component.findById('mp1'))
138
- cy.wrap(multiComp.$el).as('htmlMultiRoot')
139
- cy.wrap(multiComp.$el.querySelector('.gdrop-file')).as('htmlDropzone')
140
- })
141
-
142
- cy.get('@htmlDropzone').selectFile(
143
- { ...pngFile, fileName: 'html-mode.png' },
144
- { action: 'drag-drop' }
145
- )
146
-
147
- cy.get('@htmlMultiRoot').find('.guploaded-file .file-container').should('have.length', 1)
96
+ .should('have.length', 1);
97
+
98
+ cy.visit(htmlUploadUrl);
99
+
100
+ withComponentThen('mp1', (comp) => {
101
+ expect(comp.$el, 'html multi progress element').to.exist;
102
+ expect(comp.$el.querySelector('.gdrop-file'), 'html multi progress dropzone').to.exist;
103
+ cy.wrap(comp.$el).as('htmlMultiRoot');
104
+ cy.wrap(comp.$el.querySelector('.gdrop-file')).as('htmlDropzone');
105
+ });
106
+
107
+ cy.get('@pngContents').then((contents) => {
108
+ cy.get('@htmlDropzone').selectFile(
109
+ { contents, fileName: 'html-mode.png', mimeType: 'image/png' },
110
+ { action: 'drag-drop' }
111
+ );
112
+ });
113
+
114
+ cy.get('@htmlMultiRoot').find('.guploaded-file .file-container').should('have.length', 1);
148
115
  cy.get('@htmlMultiRoot')
149
116
  .find('input[type="hidden"][name="user[file_multiprogress1][]"]')
150
- .should('not.exist')
151
- })
117
+ .should('not.exist');
118
+ });
119
+
120
+ it('rejects files with an invalid type in multi progress view', () => {
121
+ cy.fixture('document.pdf', null).as('pdfContents');
122
+
123
+ cy.visit(url);
124
+
125
+ withComponentThen('fields_upload_multiprogress', (comp) => {
126
+ expect(comp.$el.querySelector('.gdrop-file'), 'dropzone').to.exist;
127
+ cy.wrap(comp.$el).as('multiRoot');
128
+ cy.wrap(comp.$el.querySelector('.gdrop-file')).as('dropzone');
129
+ });
130
+
131
+ cy.get('@pdfContents').then((contents) => {
132
+ cy.get('@dropzone').selectFile(
133
+ { contents, fileName: 'document.pdf', mimeType: 'application/pdf' },
134
+ { action: 'drag-drop' }
135
+ );
136
+ });
137
+ cy.get('@multiRoot').find('.guploaded-file').should('not.exist');
138
+ });
139
+
140
+ it('rejects files that exceed the maximum file size in multi progress view', () => {
141
+ // maxFileSize: 10 in the test page → 10 * 1000 = 10,000 bytes limit
142
+ cy.fixture('large.png', null).as('largeContents');
143
+
144
+ cy.visit(url);
145
+
146
+ withComponentThen('fields_upload_multiprogress', (comp) => {
147
+ expect(comp.$el.querySelector('.gdrop-file'), 'dropzone').to.exist;
148
+ cy.wrap(comp.$el).as('multiRoot');
149
+ cy.wrap(comp.$el.querySelector('.gdrop-file')).as('dropzone');
150
+ });
151
+
152
+ cy.get('@largeContents').then((contents) => {
153
+ cy.get('@dropzone').selectFile(
154
+ { contents, fileName: 'large.png', mimeType: 'image/png' },
155
+ { action: 'drag-drop' }
156
+ );
157
+ });
158
+ cy.get('@multiRoot').find('.guploaded-file').should('not.exist');
159
+ });
160
+
161
+ it('rejects uploads that exceed the maximum file count in multi progress view', () => {
162
+ let uploadIndex = 0;
163
+
164
+ cy.fixture('upload.png', null).as('pngContents');
165
+
166
+ cy.intercept('POST', /\/(glib\/glib_direct_uploads|rails\/active_storage\/direct_uploads)$/, (req) => {
167
+ uploadIndex += 1;
168
+ req.reply({
169
+ statusCode: 200,
170
+ body: {
171
+ signed_id: `signed-${uploadIndex}`,
172
+ direct_upload: {
173
+ url: 'https://example.com/rails/active_storage/blobs/upload',
174
+ headers: { 'Content-Type': 'text/plain' }
175
+ }
176
+ }
177
+ });
178
+ }).as('directUploadCreate');
179
+
180
+ cy.intercept('PUT', 'https://example.com/rails/active_storage/blobs/upload', { statusCode: 200 }).as('directUploadStore');
181
+
182
+ cy.visit(url);
183
+
184
+ withComponentThen('fields_upload_multiprogress', (comp) => {
185
+ expect(comp.$el.querySelector('.gdrop-file'), 'dropzone').to.exist;
186
+ cy.wrap(comp.$el).as('multiRoot');
187
+ cy.wrap(comp.$el.querySelector('.gdrop-file')).as('dropzone');
188
+ });
189
+
190
+ // Upload 2 files to reach maxFileLength: 2
191
+ cy.get('@pngContents').then((contents) => {
192
+ cy.get('@dropzone').selectFile(
193
+ { contents, fileName: 'first.png', mimeType: 'image/png' },
194
+ { action: 'drag-drop' }
195
+ );
196
+ });
197
+ cy.wait('@directUploadCreate');
198
+ cy.wait('@directUploadStore');
199
+ cy.get('@pngContents').then((contents) => {
200
+ cy.get('@dropzone').selectFile(
201
+ { contents, fileName: 'second.png', mimeType: 'image/png' },
202
+ { action: 'drag-drop' }
203
+ );
204
+ });
205
+ cy.wait('@directUploadCreate');
206
+ cy.wait('@directUploadStore');
207
+ cy.get('@multiRoot').find('.guploaded-file .file-container').should('have.length', 2);
208
+
209
+ // A 3rd file should be rejected — container count stays at 2
210
+ cy.get('@pngContents').then((contents) => {
211
+ cy.get('@dropzone').selectFile(
212
+ { contents, fileName: 'third.png', mimeType: 'image/png' },
213
+ { action: 'drag-drop' }
214
+ );
215
+ });
216
+ cy.get('@multiRoot').find('.guploaded-file .file-container').should('have.length', 2);
217
+ });
152
218
 
153
219
  it('submits the form', () => {
154
- cy.visit(url)
220
+ cy.visit(url);
221
+
222
+ cy.contains('submit').click();
223
+ cy.get('.unformatted').should('contain.text', 'Method: POST');
224
+ });
225
+
226
+ it('pastes an image from clipboard and shows filename', () => {
227
+ let uploadIndex = 0;
228
+
229
+ cy.intercept('POST', /\/(glib\/glib_direct_uploads|rails\/active_storage\/direct_uploads)$/, (req) => {
230
+ uploadIndex += 1;
231
+ req.reply({
232
+ statusCode: 200,
233
+ body: {
234
+ signed_id: `signed-clip-${uploadIndex}`,
235
+ direct_upload: {
236
+ url: 'https://example.com/rails/active_storage/blobs/upload',
237
+ headers: { 'Content-Type': 'text/plain' }
238
+ }
239
+ }
240
+ });
241
+ }).as('directUploadCreate');
242
+
243
+ cy.intercept('PUT', 'https://example.com/rails/active_storage/blobs/upload', {
244
+ statusCode: 200
245
+ }).as('directUploadStore');
246
+
247
+ cy.visit(url);
248
+
249
+ withComponentThen('fields_upload_clipboard', (comp) => {
250
+ cy.wrap(comp.$el).as('clipRoot');
251
+ });
252
+
253
+ cy.window().then((win) => {
254
+ const mockBlob = new win.Blob(['fake-png'], { type: 'image/png' });
255
+ cy.stub(win.navigator.clipboard, 'read').resolves([
256
+ { types: ['image/png'], getType: () => Promise.resolve(mockBlob) }
257
+ ]);
258
+ });
259
+
260
+ cy.get('@clipRoot').find('.v-input__append i').click();
261
+ cy.wait('@directUploadCreate');
262
+ cy.wait('@directUploadStore');
155
263
 
156
- cy.contains('submit').click()
157
- cy.get('.unformatted').should('contain.text', 'Method: POST')
158
- })
159
- })
264
+ cy.get('@clipRoot').find('input').should('have.value', 'clipboard.png');
265
+ cy.get('@clipRoot')
266
+ .find('input[type="hidden"][name="user[file_clipboard]"]')
267
+ .should('have.value', 'signed-clip-1');
268
+ });
269
+ });
@@ -1,53 +1,82 @@
1
- import { testPageUrl } from "../../helper.js"
1
+ import { testPageUrl } from "../../helper.js";
2
2
 
3
- const url = testPageUrl('image')
4
- const imageSelector = '#image_main'
3
+ const url = testPageUrl('image');
4
+ const imageSelector = '#image_main';
5
5
 
6
6
  const getImage = () => {
7
7
  return cy.get(imageSelector).then(($el) => {
8
- const image = $el.is('img') ? $el : $el.find('img')
9
- expect(image.length).to.be.greaterThan(0)
10
- return cy.wrap(image)
11
- })
12
- }
8
+ const image = $el.is('img') ? $el : $el.find('img');
9
+ expect(image.length).to.be.greaterThan(0);
10
+ return cy.wrap(image);
11
+ });
12
+ };
13
13
 
14
14
  describe('image', () => {
15
15
  it('updates size and fit settings', () => {
16
- cy.visit(url)
16
+ cy.visit(url);
17
17
 
18
- cy.contains('Default').click()
19
- getImage().should('have.css', 'object-fit', 'contain')
18
+ cy.contains('Default').click();
19
+ getImage().should('have.css', 'object-fit', 'contain');
20
20
 
21
- cy.contains('Crop square').click()
22
- getImage().should('have.css', 'object-fit', 'cover')
21
+ cy.contains('Crop square').click();
22
+ getImage().should('have.css', 'object-fit', 'cover');
23
23
  getImage().should(($image) => {
24
- expect($image.attr('width')).to.eq('160px')
25
- })
24
+ expect($image.attr('width')).to.eq('160px');
25
+ });
26
26
 
27
- cy.contains('Full width').click()
28
- getImage().should('have.css', 'object-fit', 'contain')
27
+ cy.contains('Full width').click();
28
+ getImage().should('have.css', 'object-fit', 'contain');
29
29
 
30
- cy.contains('Tall crop').click()
31
- getImage().should('have.css', 'object-fit', 'cover')
32
- })
30
+ cy.contains('Tall crop').click();
31
+ getImage().should('have.css', 'object-fit', 'cover');
32
+ });
33
33
 
34
34
  it('toggles badges', () => {
35
- cy.visit(url)
35
+ cy.visit(url);
36
36
 
37
- cy.contains('Show badge').click()
38
- cy.get('#image_main .v-badge__badge').should('contain.text', '3')
39
- })
37
+ cy.contains('Show badge').click();
38
+ cy.get('#image_main .v-badge__badge').should('contain.text', '3');
39
+ });
40
40
 
41
41
  it('handles tiny thumbnails', () => {
42
- cy.visit(url)
42
+ cy.visit(url);
43
43
 
44
- cy.contains('Tiny thumbnail').click()
45
- getImage().should('have.css', 'object-fit', 'cover')
44
+ cy.contains('Tiny thumbnail').click();
45
+ getImage().should('have.css', 'object-fit', 'cover');
46
46
  getImage().should(($image) => {
47
- expect($image.attr('width')).to.eq('64px')
48
- })
47
+ expect($image.attr('width')).to.eq('64px');
48
+ });
49
49
 
50
- cy.contains('Restore size').click()
51
- getImage().should('have.css', 'object-fit', 'contain')
52
- })
53
- })
50
+ cy.contains('Restore size').click();
51
+ getImage().should('have.css', 'object-fit', 'contain');
52
+ });
53
+
54
+ it('fits by height only (clip)', () => {
55
+ cy.visit(url);
56
+
57
+ cy.contains('Height clip').click();
58
+ getImage().should('have.css', 'object-fit', 'contain');
59
+ getImage().should(($image) => {
60
+ expect($image.attr('height')).to.eq('200px');
61
+ });
62
+ });
63
+
64
+ it('fits by height only (crop)', () => {
65
+ cy.visit(url);
66
+
67
+ cy.contains('Height crop').click();
68
+ getImage().should('have.css', 'object-fit', 'cover');
69
+ getImage().should(($image) => {
70
+ expect($image.attr('height')).to.eq('160px');
71
+ });
72
+ });
73
+
74
+ it('handles image onClick', () => {
75
+ cy.visit(url);
76
+
77
+ cy.on('window:alert', (message) => {
78
+ expect(message).to.include('Image clicked');
79
+ });
80
+ getImage().click();
81
+ });
82
+ });
@@ -0,0 +1,13 @@
1
+ import { testPageUrl } from "../../helper.js";
2
+
3
+ const url = testPageUrl('switch');
4
+
5
+ describe('switch', () => {
6
+ it('toggles push notifications on from default off state', () => {
7
+ cy.visit(url);
8
+
9
+ cy.contains('Push notifications').click();
10
+
11
+ cy.contains('Push enabled').should('be.visible');
12
+ });
13
+ });
@@ -0,0 +1,23 @@
1
+ import { testPageUrl } from "../../helper.js";
2
+
3
+ const url = testPageUrl('tabBar');
4
+
5
+ describe('tabBar', () => {
6
+ it('shows Tapped Alerts snackbar when Alerts tab is clicked', () => {
7
+ cy.visit(url);
8
+
9
+ cy.contains('Alerts').click();
10
+
11
+ cy.contains('Tapped Alerts').should('be.visible');
12
+ });
13
+
14
+ it('renders all-disabled tabs as unclickable', () => {
15
+ cy.visit(url);
16
+
17
+ cy.contains('All tabs disabled').should('be.visible');
18
+ cy.contains('One').should('be.visible');
19
+ cy.contains('Two').should('be.visible');
20
+ cy.contains('Three').should('be.visible');
21
+ });
22
+
23
+ });
@@ -0,0 +1,12 @@
1
+ %PDF-1.4
2
+ 1 0 obj<</Type /Catalog /Pages 2 0 R>>endobj 2 0 obj<</Type /Pages /Kids [3 0 R] /Count 1>>endobj 3 0 obj<</Type /Page /MediaBox [0 0 3 3]>>endobj
3
+ xref
4
+ 0 4
5
+ 0000000000 65535 f
6
+ 0000000009 00000 n
7
+ 0000000058 00000 n
8
+ 0000000115 00000 n
9
+ trailer<</Size 4 /Root 1 0 R>>
10
+ startxref
11
+ 190
12
+ %%EOF
Binary file
Binary file
package/cypress/helper.js CHANGED
@@ -16,4 +16,16 @@ function withComponent(id, callback) {
16
16
  })
17
17
  }
18
18
 
19
- export { testPageUrl, withComponent }
19
+ function withComponentThen(id, callback) {
20
+ return cy.window()
21
+ .should((win) => {
22
+ const comp = realComponent(win.GLib.component.findById(id))
23
+ expect(comp).to.exist
24
+ expect(comp.$el instanceof win.HTMLElement).to.be.true
25
+ })
26
+ .then((win) => {
27
+ callback(realComponent(win.GLib.component.findById(id)))
28
+ })
29
+ }
30
+
31
+ export { testPageUrl, withComponent, withComponentThen }
@@ -1,5 +1,5 @@
1
1
  import "./commands.js";
2
- import "@cypress/code-coverage/support.js";
2
+ import "@cypress/code-coverage/support";
3
3
  import { isFunction, isObject } from "../../utils/type.js";
4
4
 
5
5
  const win = window;
@@ -15,21 +15,28 @@
15
15
 
16
16
  // Import commands.js using ES2015 syntax:
17
17
  import "./commands.js";
18
- import "@cypress/code-coverage/support.js";
19
- import { isFunction, isObject } from "../../utils/type.js";
18
+ import "@cypress/code-coverage/support";
19
+ // import { isFunction, isObject } from "../../utils/type.js";
20
20
 
21
- Cypress.on("window:before:load", (win) => {
22
- if (!isObject(win) || !isObject(win.console)) return;
21
+ // Cypress.on("window:before:load", (win) => {
22
+ // if (!isObject(win) || !isObject(win.console)) return;
23
23
 
24
- if (isFunction(win.console.log)) {
25
- win.console.log = () => {};
26
- }
27
- if (isFunction(win.console.warn)) {
28
- win.console.warn = () => {};
29
- }
30
- if (isFunction(win.console.info)) {
31
- win.console.info = () => {};
32
- }
24
+ // if (isFunction(win.console.log)) {
25
+ // win.console.log = () => {};
26
+ // }
27
+ // if (isFunction(win.console.warn)) {
28
+ // win.console.warn = () => {};
29
+ // }
30
+ // if (isFunction(win.console.info)) {
31
+ // win.console.info = () => {};
32
+ // }
33
+ // });
34
+
35
+ // Stripe and Google Maps telemetry requests block cy.visit() in Cypress 15+ due
36
+ // to stricter network idle detection. Stub them globally so they don't stall tests.
37
+ beforeEach(() => {
38
+ cy.intercept('https://m.stripe.com/**', { statusCode: 200, body: {}, log: false },);
39
+ cy.intercept('https://maps.googleapis.com/**', { statusCode: 200, body: {}, log: false });
33
40
  });
34
41
 
35
42
  // Ignore retry failures from backend error pages so specs can assert UI state.
@@ -0,0 +1,22 @@
1
+ ## Handling Dependabot Dependency Updates
2
+
3
+ ### Review the changelog first
4
+ Before merging, check what changed between versions — especially for major bumps. Look for breaking changes, deprecated APIs, or behavior changes.
5
+
6
+ ### Understand semver signals
7
+ - **Patch (1.0.x)** — bug/security fixes, generally safe to merge
8
+ - **Minor (1.x.0)** — new features, backward-compatible, low risk
9
+ - **Major (x.0.0)** — breaking changes likely, requires careful review and testing
10
+
11
+ The general rule: patch = merge quickly, minor = review and merge, major = treat like a migration task.
12
+
13
+ ### Don't skip tests
14
+ Make sure CI runs the full test suite on each Dependabot PR before merging. If tests are absent, do a manual smoke test of critical paths.
15
+
16
+ For **minor version updates**, run the Cypress test suite locally before merging (`yarn test`). New features in a minor bump can introduce subtle behavior changes that unit tests won't catch.
17
+
18
+ ### Check for dependency compatibility
19
+ Bumping one package can cause peer dependency conflicts with others. Review the `yarn.lock` diff — a surprisingly large change warrants a closer look. Also check that the updated package is compatible with other key dependencies (e.g. a Vite plugin bumped to support Vite 6 may break if you're still on Vite 5).
20
+
21
+ ### Don't blindly auto-merge everything
22
+ Auto-merge is reasonable for patch-level updates in well-tested projects, but be cautious with minor/major bumps or packages central to the app (e.g. web framework, auth libraries).