bard-attachment_field 0.1.0
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.
- checksums.yaml +7 -0
- data/.nvmrc +1 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/Appraisals +17 -0
- data/CHANGELOG.md +15 -0
- data/Gemfile +6 -0
- data/LICENSE +21 -0
- data/README.md +39 -0
- data/Rakefile +10 -0
- data/app/assets/javascripts/input-attachment.js +6021 -0
- data/app/controllers/bard/attachment_field/blobs_controller.rb +11 -0
- data/bard-attachment_field.gemspec +56 -0
- data/config/cucumber.yml +1 -0
- data/config/routes.rb +6 -0
- data/gemfiles/rails_7.1.gemfile +7 -0
- data/gemfiles/rails_7.2.gemfile +7 -0
- data/gemfiles/rails_8.0.gemfile +7 -0
- data/gemfiles/rails_8.1.gemfile +7 -0
- data/input-attachment/.editorconfig +15 -0
- data/input-attachment/.github/workflows/test.yml +21 -0
- data/input-attachment/.gitignore +27 -0
- data/input-attachment/.prettierrc.json +13 -0
- data/input-attachment/CLAUDE.md +63 -0
- data/input-attachment/LICENSE +21 -0
- data/input-attachment/README.md +288 -0
- data/input-attachment/bin/log +2 -0
- data/input-attachment/bin/server +1 -0
- data/input-attachment/bin/setup +4 -0
- data/input-attachment/bun.lockb +0 -0
- data/input-attachment/bundle.js +3 -0
- data/input-attachment/jest-setup.js +24 -0
- data/input-attachment/package.json +56 -0
- data/input-attachment/src/components/attachment-file/accepts.ts +32 -0
- data/input-attachment/src/components/attachment-file/attachment-file.css +89 -0
- data/input-attachment/src/components/attachment-file/attachment-file.e2e.ts +11 -0
- data/input-attachment/src/components/attachment-file/attachment-file.spec.tsx +20 -0
- data/input-attachment/src/components/attachment-file/attachment-file.tsx +157 -0
- data/input-attachment/src/components/attachment-file/direct-upload-controller.tsx +100 -0
- data/input-attachment/src/components/attachment-file/extensions.ts +13 -0
- data/input-attachment/src/components/attachment-file/max.ts +46 -0
- data/input-attachment/src/components/attachment-file/readme.md +55 -0
- data/input-attachment/src/components/attachment-preview/attachment-preview.css +8 -0
- data/input-attachment/src/components/attachment-preview/attachment-preview.e2e.ts +11 -0
- data/input-attachment/src/components/attachment-preview/attachment-preview.spec.tsx +19 -0
- data/input-attachment/src/components/attachment-preview/attachment-preview.tsx +42 -0
- data/input-attachment/src/components/attachment-preview/readme.md +31 -0
- data/input-attachment/src/components/input-attachment/form-controller.tsx +146 -0
- data/input-attachment/src/components/input-attachment/input-attachment.css +100 -0
- data/input-attachment/src/components/input-attachment/input-attachment.e2e.ts +11 -0
- data/input-attachment/src/components/input-attachment/input-attachment.spec.tsx +37 -0
- data/input-attachment/src/components/input-attachment/input-attachment.tsx +353 -0
- data/input-attachment/src/components/input-attachment/readme.md +45 -0
- data/input-attachment/src/components.d.ts +175 -0
- data/input-attachment/src/global.d.ts +3 -0
- data/input-attachment/src/images/example.jpg +0 -0
- data/input-attachment/src/index.html +36 -0
- data/input-attachment/src/index.ts +1 -0
- data/input-attachment/src/utils/utils.spec.ts +19 -0
- data/input-attachment/src/utils/utils.ts +14 -0
- data/input-attachment/stencil.config.ts +43 -0
- data/input-attachment/test-mocks/file-drop.cjs +7 -0
- data/input-attachment/test-mocks/progress-bar.cjs +9 -0
- data/input-attachment/tsconfig.json +32 -0
- data/lib/bard/attachment_field/cucumber.rb +277 -0
- data/lib/bard/attachment_field/field.rb +33 -0
- data/lib/bard/attachment_field/form_builder.rb +12 -0
- data/lib/bard/attachment_field/version.rb +7 -0
- data/lib/bard/attachment_field.rb +20 -0
- data/lib/bard-attachment_field.rb +1 -0
- metadata +409 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
/* tslint:disable */
|
|
3
|
+
/**
|
|
4
|
+
* This is an autogenerated file created by the Stencil compiler.
|
|
5
|
+
* It contains typing information for all components that exist in this project.
|
|
6
|
+
*/
|
|
7
|
+
import { HTMLStencilElement, JSXBase } from "@stencil/core/internal";
|
|
8
|
+
export namespace Components {
|
|
9
|
+
interface AttachmentFile {
|
|
10
|
+
"accepts": string;
|
|
11
|
+
"filename": string;
|
|
12
|
+
"filetype": string;
|
|
13
|
+
"max": number;
|
|
14
|
+
"name": string;
|
|
15
|
+
/**
|
|
16
|
+
* @default 100
|
|
17
|
+
*/
|
|
18
|
+
"percent": number;
|
|
19
|
+
/**
|
|
20
|
+
* @default true
|
|
21
|
+
*/
|
|
22
|
+
"preview": boolean;
|
|
23
|
+
"size": number;
|
|
24
|
+
"src": string;
|
|
25
|
+
/**
|
|
26
|
+
* @default "complete"
|
|
27
|
+
*/
|
|
28
|
+
"state": string;
|
|
29
|
+
"url": string;
|
|
30
|
+
"validationMessage": string;
|
|
31
|
+
/**
|
|
32
|
+
* @default ""
|
|
33
|
+
*/
|
|
34
|
+
"value": string;
|
|
35
|
+
}
|
|
36
|
+
interface AttachmentPreview {
|
|
37
|
+
"filetype": string;
|
|
38
|
+
"src": string;
|
|
39
|
+
}
|
|
40
|
+
interface InputAttachment {
|
|
41
|
+
"accepts": string;
|
|
42
|
+
"directupload": string;
|
|
43
|
+
/**
|
|
44
|
+
* @default false
|
|
45
|
+
*/
|
|
46
|
+
"disabled": boolean;
|
|
47
|
+
"max": number;
|
|
48
|
+
/**
|
|
49
|
+
* @default false
|
|
50
|
+
*/
|
|
51
|
+
"multiple": boolean;
|
|
52
|
+
"name": string;
|
|
53
|
+
/**
|
|
54
|
+
* @default true
|
|
55
|
+
*/
|
|
56
|
+
"preview": boolean;
|
|
57
|
+
/**
|
|
58
|
+
* @default false
|
|
59
|
+
*/
|
|
60
|
+
"required": boolean;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export interface AttachmentFileCustomEvent<T> extends CustomEvent<T> {
|
|
64
|
+
detail: T;
|
|
65
|
+
target: HTMLAttachmentFileElement;
|
|
66
|
+
}
|
|
67
|
+
declare global {
|
|
68
|
+
interface HTMLAttachmentFileElementEventMap {
|
|
69
|
+
"attachment-file:remove": any;
|
|
70
|
+
"attachment-file:validation": any;
|
|
71
|
+
}
|
|
72
|
+
interface HTMLAttachmentFileElement extends Components.AttachmentFile, HTMLStencilElement {
|
|
73
|
+
addEventListener<K extends keyof HTMLAttachmentFileElementEventMap>(type: K, listener: (this: HTMLAttachmentFileElement, ev: AttachmentFileCustomEvent<HTMLAttachmentFileElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
|
|
74
|
+
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
|
75
|
+
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
|
76
|
+
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
|
|
77
|
+
removeEventListener<K extends keyof HTMLAttachmentFileElementEventMap>(type: K, listener: (this: HTMLAttachmentFileElement, ev: AttachmentFileCustomEvent<HTMLAttachmentFileElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
|
|
78
|
+
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
|
79
|
+
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
|
80
|
+
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
|
|
81
|
+
}
|
|
82
|
+
var HTMLAttachmentFileElement: {
|
|
83
|
+
prototype: HTMLAttachmentFileElement;
|
|
84
|
+
new (): HTMLAttachmentFileElement;
|
|
85
|
+
};
|
|
86
|
+
interface HTMLAttachmentPreviewElement extends Components.AttachmentPreview, HTMLStencilElement {
|
|
87
|
+
}
|
|
88
|
+
var HTMLAttachmentPreviewElement: {
|
|
89
|
+
prototype: HTMLAttachmentPreviewElement;
|
|
90
|
+
new (): HTMLAttachmentPreviewElement;
|
|
91
|
+
};
|
|
92
|
+
interface HTMLInputAttachmentElement extends Components.InputAttachment, HTMLStencilElement {
|
|
93
|
+
}
|
|
94
|
+
var HTMLInputAttachmentElement: {
|
|
95
|
+
prototype: HTMLInputAttachmentElement;
|
|
96
|
+
new (): HTMLInputAttachmentElement;
|
|
97
|
+
};
|
|
98
|
+
interface HTMLElementTagNameMap {
|
|
99
|
+
"attachment-file": HTMLAttachmentFileElement;
|
|
100
|
+
"attachment-preview": HTMLAttachmentPreviewElement;
|
|
101
|
+
"input-attachment": HTMLInputAttachmentElement;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
declare namespace LocalJSX {
|
|
105
|
+
interface AttachmentFile {
|
|
106
|
+
"accepts"?: string;
|
|
107
|
+
"filename"?: string;
|
|
108
|
+
"filetype"?: string;
|
|
109
|
+
"max"?: number;
|
|
110
|
+
"name"?: string;
|
|
111
|
+
"onAttachment-file:remove"?: (event: AttachmentFileCustomEvent<any>) => void;
|
|
112
|
+
"onAttachment-file:validation"?: (event: AttachmentFileCustomEvent<any>) => void;
|
|
113
|
+
/**
|
|
114
|
+
* @default 100
|
|
115
|
+
*/
|
|
116
|
+
"percent"?: number;
|
|
117
|
+
/**
|
|
118
|
+
* @default true
|
|
119
|
+
*/
|
|
120
|
+
"preview"?: boolean;
|
|
121
|
+
"size"?: number;
|
|
122
|
+
"src"?: string;
|
|
123
|
+
/**
|
|
124
|
+
* @default "complete"
|
|
125
|
+
*/
|
|
126
|
+
"state"?: string;
|
|
127
|
+
"url"?: string;
|
|
128
|
+
"validationMessage"?: string;
|
|
129
|
+
/**
|
|
130
|
+
* @default ""
|
|
131
|
+
*/
|
|
132
|
+
"value"?: string;
|
|
133
|
+
}
|
|
134
|
+
interface AttachmentPreview {
|
|
135
|
+
"filetype"?: string;
|
|
136
|
+
"src"?: string;
|
|
137
|
+
}
|
|
138
|
+
interface InputAttachment {
|
|
139
|
+
"accepts"?: string;
|
|
140
|
+
"directupload"?: string;
|
|
141
|
+
/**
|
|
142
|
+
* @default false
|
|
143
|
+
*/
|
|
144
|
+
"disabled"?: boolean;
|
|
145
|
+
"max"?: number;
|
|
146
|
+
/**
|
|
147
|
+
* @default false
|
|
148
|
+
*/
|
|
149
|
+
"multiple"?: boolean;
|
|
150
|
+
"name"?: string;
|
|
151
|
+
/**
|
|
152
|
+
* @default true
|
|
153
|
+
*/
|
|
154
|
+
"preview"?: boolean;
|
|
155
|
+
/**
|
|
156
|
+
* @default false
|
|
157
|
+
*/
|
|
158
|
+
"required"?: boolean;
|
|
159
|
+
}
|
|
160
|
+
interface IntrinsicElements {
|
|
161
|
+
"attachment-file": AttachmentFile;
|
|
162
|
+
"attachment-preview": AttachmentPreview;
|
|
163
|
+
"input-attachment": InputAttachment;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
export { LocalJSX as JSX };
|
|
167
|
+
declare module "@stencil/core" {
|
|
168
|
+
export namespace JSX {
|
|
169
|
+
interface IntrinsicElements {
|
|
170
|
+
"attachment-file": LocalJSX.AttachmentFile & JSXBase.HTMLAttributes<HTMLAttachmentFileElement>;
|
|
171
|
+
"attachment-preview": LocalJSX.AttachmentPreview & JSXBase.HTMLAttributes<HTMLAttachmentPreviewElement>;
|
|
172
|
+
"input-attachment": LocalJSX.InputAttachment & JSXBase.HTMLAttributes<HTMLInputAttachmentElement>;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html dir="ltr" lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=5.0" />
|
|
6
|
+
<title><input-attachment> and friends</title>
|
|
7
|
+
<script type="importmap">
|
|
8
|
+
{ "imports": {
|
|
9
|
+
"@stencil/core": "https://ga.jspm.io/npm:@stencil/core@4.8.0/internal/stencil-core/index.js",
|
|
10
|
+
"@stencil/core/internal/app-data": "https://ga.jspm.io/npm:@stencil/core@4.8.0/internal/app-data/index.js",
|
|
11
|
+
"@stencil/core/internal/client": "https://ga.jspm.io/npm:@stencil/core@4.8.0/internal/client/index.js"
|
|
12
|
+
} }
|
|
13
|
+
</script>
|
|
14
|
+
<script type="module" src="./build/input-attachment.esm.js"></script>
|
|
15
|
+
</head>
|
|
16
|
+
<body style="background: #f0f0f0;">
|
|
17
|
+
<fieldset>
|
|
18
|
+
<legend><input-attachment></legend>
|
|
19
|
+
<form>
|
|
20
|
+
<input-attachment name="image" id="pep" directupload="http://smh.localhost/rails/active_storage/direct_uploads" accepts="image,video">
|
|
21
|
+
<attachment-file name="image" src="images/example.jpg" filename="example1.jpg" size="36287" max="1000000"></attachment-file>
|
|
22
|
+
</input-attachment>
|
|
23
|
+
</form>
|
|
24
|
+
</fieldset>
|
|
25
|
+
|
|
26
|
+
<fieldset>
|
|
27
|
+
<legend><attachment-file></legend>
|
|
28
|
+
<attachment-file name="image" src="images/example.jpg" filename="example.jpg" size="36287" filetype="image" max="1000000"></attachment-file>
|
|
29
|
+
</fieldset>
|
|
30
|
+
|
|
31
|
+
<fieldset>
|
|
32
|
+
<legend><attachment-preview></legend>
|
|
33
|
+
<attachment-preview src="images/example.jpg" filetype="image"></attachment-preview>
|
|
34
|
+
</fieldset>
|
|
35
|
+
</body>
|
|
36
|
+
</html>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './components';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { html, arrayRemove } from './utils';
|
|
2
|
+
|
|
3
|
+
describe('utils', () => {
|
|
4
|
+
describe('html', () => {
|
|
5
|
+
it('creates DOM elements from HTML string', () => {
|
|
6
|
+
const element = html('<div>test</div>');
|
|
7
|
+
expect(element.tagName).toBe('DIV');
|
|
8
|
+
expect(element.textContent).toBe('test');
|
|
9
|
+
});
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe('arrayRemove', () => {
|
|
13
|
+
it('removes first occurrence of item from array', () => {
|
|
14
|
+
const arr = [1, 2, 3, 4, 3];
|
|
15
|
+
arrayRemove(arr, 3);
|
|
16
|
+
expect(arr).toEqual([1, 2, 4, 3]);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
function html(html) {
|
|
2
|
+
const el = document.createElement("div")
|
|
3
|
+
el.innerHTML = html
|
|
4
|
+
return el.children[0]
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function arrayRemove(arr, e) {
|
|
8
|
+
const index = arr.findIndex(x => x === e)
|
|
9
|
+
if(index !== -1) {
|
|
10
|
+
arr.splice(index, 1)
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export { html, arrayRemove }
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Config } from '@stencil/core';
|
|
2
|
+
|
|
3
|
+
export const config: Config = {
|
|
4
|
+
namespace: 'input-attachment',
|
|
5
|
+
outputTargets: [
|
|
6
|
+
{
|
|
7
|
+
type: 'dist-custom-elements',
|
|
8
|
+
dir: 'dist/components',
|
|
9
|
+
customElementsExportBehavior: 'bundle',
|
|
10
|
+
isPrimaryPackageOutputTarget: true,
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
type: 'www',
|
|
14
|
+
dir: 'www',
|
|
15
|
+
serviceWorker: null,
|
|
16
|
+
copy: [
|
|
17
|
+
{ src: 'images', dest: 'images' }
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
type: 'docs-readme',
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
devServer: {
|
|
25
|
+
reloadStrategy: 'pageReload',
|
|
26
|
+
root: 'www',
|
|
27
|
+
},
|
|
28
|
+
testing: {
|
|
29
|
+
browserHeadless: "new",
|
|
30
|
+
useESModules: true,
|
|
31
|
+
moduleNameMapper: {
|
|
32
|
+
"^@botandrose/progress-bar$": "<rootDir>/test-mocks/progress-bar.cjs",
|
|
33
|
+
"^@botandrose/file-drop$": "<rootDir>/test-mocks/file-drop.cjs"
|
|
34
|
+
},
|
|
35
|
+
transformIgnorePatterns: [
|
|
36
|
+
"node_modules/(?!(rails-request-json|@botandrose/progress-bar|@botandrose/file-drop|@rails/request\.js))"
|
|
37
|
+
],
|
|
38
|
+
testPathIgnorePatterns: ["/node_modules/", "/dist/"],
|
|
39
|
+
setupFilesAfterEnv: ["<rootDir>/jest-setup.js"],
|
|
40
|
+
browserArgs: process.env.CI ? ['--no-sandbox', '--disable-setuid-sandbox'] : []
|
|
41
|
+
},
|
|
42
|
+
validatePrimaryPackageOutputTarget: true,
|
|
43
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Mock for @botandrose/progress-bar in Jest tests (CommonJS)
|
|
2
|
+
module.exports = {};
|
|
3
|
+
if (typeof customElements !== 'undefined' && !customElements.get('progress-bar')) {
|
|
4
|
+
customElements.define('progress-bar', class extends HTMLElement {
|
|
5
|
+
static get observedAttributes() { return ['value', 'max']; }
|
|
6
|
+
connectedCallback() {}
|
|
7
|
+
attributeChangedCallback() {}
|
|
8
|
+
});
|
|
9
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"allowSyntheticDefaultImports": true,
|
|
4
|
+
"allowUnreachableCode": false,
|
|
5
|
+
"declaration": false,
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
"experimentalDecorators": true,
|
|
8
|
+
"lib": [
|
|
9
|
+
"dom",
|
|
10
|
+
"esnext"
|
|
11
|
+
],
|
|
12
|
+
"module": "esnext",
|
|
13
|
+
"moduleResolution": "node",
|
|
14
|
+
"target": "esnext",
|
|
15
|
+
"skipLibCheck": true,
|
|
16
|
+
"noUnusedLocals": true,
|
|
17
|
+
"noUnusedParameters": true,
|
|
18
|
+
"jsx": "react",
|
|
19
|
+
"jsxFactory": "h",
|
|
20
|
+
"jsxFragmentFactory": "Fragment",
|
|
21
|
+
"types": []
|
|
22
|
+
},
|
|
23
|
+
"include": [
|
|
24
|
+
"src"
|
|
25
|
+
],
|
|
26
|
+
"exclude": [
|
|
27
|
+
"**/*.spec.ts",
|
|
28
|
+
"**/*.spec.tsx",
|
|
29
|
+
"**/*.e2e.ts",
|
|
30
|
+
"**/*.e2e.tsx"
|
|
31
|
+
]
|
|
32
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# Cucumber integration for bard-attachment_field
|
|
2
|
+
#
|
|
3
|
+
# Usage: require "bard/attachment_field/cucumber" in your features/support/env.rb
|
|
4
|
+
#
|
|
5
|
+
# Provides step definitions and helpers for testing attachment fields.
|
|
6
|
+
#
|
|
7
|
+
# Note: CDP's DOM.setFileInputFiles silently fails for file inputs inside shadow DOM.
|
|
8
|
+
# This integration uses a workaround that creates a temporary regular DOM input to
|
|
9
|
+
# receive files via CDP, then transfers them to the component via its addFiles() method.
|
|
10
|
+
|
|
11
|
+
require "active_support/core_ext/module/attribute_accessors"
|
|
12
|
+
require "chop"
|
|
13
|
+
require "rspec/expectations"
|
|
14
|
+
|
|
15
|
+
module Bard::AttachmentField::TestHelper
|
|
16
|
+
mattr_accessor :fixtures_path do
|
|
17
|
+
-> { Rails.root.join("features/support/fixtures") }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
def resolve_fixture_path(filename)
|
|
23
|
+
File.expand_path(filename, fixtures_path.call)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def find_field(session, label)
|
|
27
|
+
label_element = session.find("label", text: /^#{Regexp.escape(label)}$/)
|
|
28
|
+
element_id = label_element[:for]
|
|
29
|
+
session.find("input-attachment##{element_id}")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# CDP's setFileInputFiles fails silently for file inputs inside shadow DOM,
|
|
33
|
+
# so we use a temp regular DOM input and transfer files via JavaScript.
|
|
34
|
+
def attach_files(session, element_id, file_paths)
|
|
35
|
+
session.execute_script("document.body.insertAdjacentHTML('beforeend', '<input type=\"file\" id=\"_cdp_file_helper\" multiple style=\"display:none\">')")
|
|
36
|
+
|
|
37
|
+
temp_input = session.find("#_cdp_file_helper", visible: :all)
|
|
38
|
+
temp_input.native.node.select_file(file_paths)
|
|
39
|
+
|
|
40
|
+
session.execute_script(<<~JS)
|
|
41
|
+
(() => {
|
|
42
|
+
const temp = document.getElementById('_cdp_file_helper');
|
|
43
|
+
const host = document.getElementById('#{element_id}');
|
|
44
|
+
host.addFiles(temp.files);
|
|
45
|
+
temp.remove();
|
|
46
|
+
})()
|
|
47
|
+
JS
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def wait_for_upload(session, element_id, timeout: 30)
|
|
51
|
+
session.document.synchronize(timeout, errors: [RuntimeError]) do
|
|
52
|
+
states = session.evaluate_script(<<~JS)
|
|
53
|
+
Array.from(document.getElementById('#{element_id}').querySelectorAll('attachment-file')).map(e => e.getAttribute('state'))
|
|
54
|
+
JS
|
|
55
|
+
raise "Uploads not complete (states=#{states})" unless states.all? { |s| s == "complete" || s == "error" }
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def get_files(field)
|
|
60
|
+
field.all("attachment-file").map { |e| e[:filename] }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def validation_messages(session, element)
|
|
64
|
+
messages = []
|
|
65
|
+
messages << element.evaluate_script("this.validationMessage")
|
|
66
|
+
# Get validation errors from attachment-file elements' shadow DOM
|
|
67
|
+
child_errors = session.evaluate_script(<<~JS, element[:id])
|
|
68
|
+
((elementId) => {
|
|
69
|
+
const host = document.getElementById(elementId);
|
|
70
|
+
const files = host.querySelectorAll('attachment-file');
|
|
71
|
+
return Array.from(files).map(f => {
|
|
72
|
+
const errorEl = f.shadowRoot?.querySelector('.validation-error');
|
|
73
|
+
return errorEl?.textContent || '';
|
|
74
|
+
}).filter(e => e);
|
|
75
|
+
})(arguments[0])
|
|
76
|
+
JS
|
|
77
|
+
messages.concat(child_errors)
|
|
78
|
+
messages.select { |m| m && !m.empty? }
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Chop integration for form diffing and filling
|
|
83
|
+
class Chop::Form::AttachmentField < Chop::Form::Field
|
|
84
|
+
def self.css_selector
|
|
85
|
+
"input-attachment"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def matches?
|
|
89
|
+
field.tag_name == "input-attachment"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def get_value
|
|
93
|
+
Bard::AttachmentField::TestHelper.get_files(field)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def diff_value
|
|
97
|
+
get_value.join(", ")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def set_value
|
|
101
|
+
filenames = if field[:multiple]
|
|
102
|
+
value.to_s.split(", ").map(&:strip)
|
|
103
|
+
else
|
|
104
|
+
[value.to_s.strip]
|
|
105
|
+
end
|
|
106
|
+
filenames.reject(&:empty?)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def fill_in!
|
|
110
|
+
return if set_value.empty?
|
|
111
|
+
|
|
112
|
+
file_paths = set_value.map do |filename|
|
|
113
|
+
::File.expand_path(::File.join(path, filename)).tap do |full_path|
|
|
114
|
+
::File.open(full_path) {} # raise Errno::ENOENT if file doesn't exist
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
Bard::AttachmentField::TestHelper.attach_files(session, field[:id], file_paths)
|
|
119
|
+
Bard::AttachmentField::TestHelper.wait_for_upload(session, field[:id])
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Step definitions
|
|
124
|
+
|
|
125
|
+
When "I attach the file {string} to {string}" do |path, field|
|
|
126
|
+
element = Bard::AttachmentField::TestHelper.find_field(page, field)
|
|
127
|
+
file_path_full = Bard::AttachmentField::TestHelper.resolve_fixture_path(path)
|
|
128
|
+
|
|
129
|
+
Bard::AttachmentField::TestHelper.attach_files(page, element[:id], [file_path_full])
|
|
130
|
+
|
|
131
|
+
page.document.synchronize(15, errors: [Capybara::ElementNotFound]) do
|
|
132
|
+
element.find("attachment-file")
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
Bard::AttachmentField::TestHelper.wait_for_upload(page, element[:id])
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
When "I attach the following files to {string}:" do |field, table|
|
|
139
|
+
files = table.raw.map(&:first).map { |filename| Bard::AttachmentField::TestHelper.resolve_fixture_path(filename) }
|
|
140
|
+
|
|
141
|
+
element = Bard::AttachmentField::TestHelper.find_field(page, field)
|
|
142
|
+
|
|
143
|
+
Bard::AttachmentField::TestHelper.attach_files(page, element[:id], files)
|
|
144
|
+
|
|
145
|
+
page.document.synchronize(15, errors: [Capybara::ElementNotFound]) do
|
|
146
|
+
element.all("attachment-file", minimum: files.length)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
Bard::AttachmentField::TestHelper.wait_for_upload(page, element[:id], timeout: 60)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
When "I remove the file from {string}" do |field|
|
|
153
|
+
element = Bard::AttachmentField::TestHelper.find_field(page, field)
|
|
154
|
+
element_id = element[:id]
|
|
155
|
+
# Use JavaScript to click the remove link since Cuprite has issues with shadow DOM paths
|
|
156
|
+
page.execute_script(<<~JS, element_id)
|
|
157
|
+
((elementId) => {
|
|
158
|
+
const host = document.getElementById(elementId);
|
|
159
|
+
const attachmentFile = host.querySelector('attachment-file');
|
|
160
|
+
const removeLink = attachmentFile.shadowRoot.querySelector('a.remove-media');
|
|
161
|
+
removeLink.click();
|
|
162
|
+
})(arguments[0])
|
|
163
|
+
JS
|
|
164
|
+
# Wait for the file to be removed (refetch element to avoid stale reference)
|
|
165
|
+
page.document.synchronize(5, errors: [Capybara::ElementNotFound]) do
|
|
166
|
+
fresh_element = page.find("##{element_id}")
|
|
167
|
+
expect(fresh_element).to have_no_css("attachment-file")
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
When "I remove {string} from {string}" do |filename, field|
|
|
172
|
+
element = Bard::AttachmentField::TestHelper.find_field(page, field)
|
|
173
|
+
element_id = element[:id]
|
|
174
|
+
# Use JavaScript to click the remove link since Cuprite has issues with shadow DOM paths
|
|
175
|
+
page.execute_script(<<~JS, element_id, filename)
|
|
176
|
+
((elementId, filename) => {
|
|
177
|
+
const host = document.getElementById(elementId);
|
|
178
|
+
const attachmentFile = host.querySelector(`attachment-file[filename='${filename}']`);
|
|
179
|
+
const removeLink = attachmentFile.shadowRoot.querySelector('a.remove-media');
|
|
180
|
+
removeLink.click();
|
|
181
|
+
})(arguments[0], arguments[1])
|
|
182
|
+
JS
|
|
183
|
+
# Wait for the file to be removed (refetch element to avoid stale reference)
|
|
184
|
+
page.document.synchronize(5, errors: [Capybara::ElementNotFound]) do
|
|
185
|
+
fresh_element = page.find("##{element_id}")
|
|
186
|
+
expect(fresh_element).to have_no_css("attachment-file[filename='#{filename}']")
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
When "I drag the file {string} onto the {string} attachment field" do |path, field|
|
|
191
|
+
element = Bard::AttachmentField::TestHelper.find_field(page, field)
|
|
192
|
+
file_path_full = Bard::AttachmentField::TestHelper.resolve_fixture_path(path)
|
|
193
|
+
|
|
194
|
+
# Create temp input to get File objects via CDP, then dispatch drop event on file-drop
|
|
195
|
+
page.execute_script("document.body.insertAdjacentHTML('beforeend', '<input type=\"file\" id=\"_cdp_file_helper\" multiple style=\"display:none\">')")
|
|
196
|
+
temp_input = page.find("#_cdp_file_helper", visible: :all)
|
|
197
|
+
temp_input.native.node.select_file([file_path_full])
|
|
198
|
+
|
|
199
|
+
page.execute_script(<<~JS, element[:id])
|
|
200
|
+
((elementId) => {
|
|
201
|
+
const temp = document.getElementById('_cdp_file_helper');
|
|
202
|
+
const host = document.getElementById(elementId);
|
|
203
|
+
const fileDrop = host.shadowRoot.querySelector('file-drop');
|
|
204
|
+
|
|
205
|
+
const dt = new DataTransfer();
|
|
206
|
+
Array.from(temp.files).forEach(f => dt.items.add(f));
|
|
207
|
+
|
|
208
|
+
const dropEvent = new DragEvent('drop', {
|
|
209
|
+
bubbles: true,
|
|
210
|
+
cancelable: true,
|
|
211
|
+
dataTransfer: dt
|
|
212
|
+
});
|
|
213
|
+
fileDrop.dispatchEvent(dropEvent);
|
|
214
|
+
temp.remove();
|
|
215
|
+
})(arguments[0])
|
|
216
|
+
JS
|
|
217
|
+
|
|
218
|
+
page.document.synchronize(15, errors: [Capybara::ElementNotFound]) do
|
|
219
|
+
element.find("attachment-file")
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
Bard::AttachmentField::TestHelper.wait_for_upload(page, element[:id])
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
Then "I should see a preview of {string} for the {string} field" do |filename, field|
|
|
226
|
+
element = Bard::AttachmentField::TestHelper.find_field(page, field)
|
|
227
|
+
expect(element.find("attachment-file")[:filename]).to eq(filename)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
When "I follow the {string} download link for {string}" do |filename, field|
|
|
231
|
+
element = Bard::AttachmentField::TestHelper.find_field(page, field)
|
|
232
|
+
# Click the download link inside attachment-file's shadow DOM
|
|
233
|
+
page.execute_script(<<~JS, element[:id], filename)
|
|
234
|
+
((elementId, filename) => {
|
|
235
|
+
const host = document.getElementById(elementId);
|
|
236
|
+
const attachmentFile = host.querySelector(`attachment-file[filename='${filename}']`) ||
|
|
237
|
+
host.querySelector('attachment-file');
|
|
238
|
+
const downloadLink = attachmentFile.shadowRoot.querySelector('a.download-link');
|
|
239
|
+
downloadLink.click();
|
|
240
|
+
})(arguments[0], arguments[1])
|
|
241
|
+
JS
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
Then "the {string} attachment field should be disabled" do |field|
|
|
245
|
+
element = Bard::AttachmentField::TestHelper.find_field(page, field)
|
|
246
|
+
|
|
247
|
+
is_disabled = page.evaluate_script(<<~JS)
|
|
248
|
+
(function() {
|
|
249
|
+
const element = document.getElementById('#{element[:id]}');
|
|
250
|
+
if (element.hasAttribute('disabled')) return true;
|
|
251
|
+
if (element.closest('fieldset[disabled]')) return true;
|
|
252
|
+
const fileInput = element.shadowRoot?.querySelector('input[type="file"]');
|
|
253
|
+
return fileInput?.disabled || false;
|
|
254
|
+
})()
|
|
255
|
+
JS
|
|
256
|
+
|
|
257
|
+
expect(is_disabled).to be true
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
Then "the {string} attachment field should have a validation error containing {string}" do |field, message|
|
|
261
|
+
element = Bard::AttachmentField::TestHelper.find_field(page, field)
|
|
262
|
+
messages = Bard::AttachmentField::TestHelper.validation_messages(page, element)
|
|
263
|
+
expect(messages).to include(a_string_including(message))
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
Then "the {string} attachment field should have no validation errors" do |field|
|
|
267
|
+
element = Bard::AttachmentField::TestHelper.find_field(page, field)
|
|
268
|
+
messages = Bard::AttachmentField::TestHelper.validation_messages(page, element)
|
|
269
|
+
expect(messages).to be_empty
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
Then "I should not be able to attach a file to {string}" do |field|
|
|
273
|
+
element = Bard::AttachmentField::TestHelper.find_field(page, field)
|
|
274
|
+
shadow_root = element.shadow_root
|
|
275
|
+
file_input = shadow_root.find("input[type='file']", visible: :all)
|
|
276
|
+
expect(file_input).to be_disabled
|
|
277
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module Bard
|
|
2
|
+
module AttachmentField
|
|
3
|
+
class Field < ActionView::Helpers::Tags::TextField
|
|
4
|
+
def render &block
|
|
5
|
+
options = @options.stringify_keys.reverse_merge({
|
|
6
|
+
"directupload" => "/rails/active_storage/direct_uploads",
|
|
7
|
+
"preview" => true,
|
|
8
|
+
})
|
|
9
|
+
add_default_name_and_id(options)
|
|
10
|
+
|
|
11
|
+
content_tag("input-attachment", options) do
|
|
12
|
+
next block.call(options) if block
|
|
13
|
+
Array(object.try(@method_name)).map do |attachment|
|
|
14
|
+
content_tag("attachment-file", nil, {
|
|
15
|
+
name: options["name"],
|
|
16
|
+
src: blob_path(attachment),
|
|
17
|
+
filename: attachment.filename,
|
|
18
|
+
value: attachment.signed_id,
|
|
19
|
+
preview: options["preview"],
|
|
20
|
+
})
|
|
21
|
+
end.join("\n").html_safe
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def blob_path(attachment)
|
|
28
|
+
"/rails/active_storage/blobs/redirect/#{attachment.signed_id}/#{attachment.filename}"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
require_relative "field"
|
|
2
|
+
|
|
3
|
+
module Bard
|
|
4
|
+
module AttachmentField
|
|
5
|
+
module FormBuilder
|
|
6
|
+
def attachment_field method, options={}, &block
|
|
7
|
+
self.multipart = true
|
|
8
|
+
Field.new(@object_name, method, @template, objectify_options(options)).render(&block)
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|