@lilaquadrat/design-core 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.
- package/README.md +236 -0
- package/html/index.html +25 -0
- package/html/index.mail.html +15 -0
- package/html/index.server.html +29 -0
- package/package.json +148 -0
- package/src/client-entry.ts +28 -0
- package/src/components/partials/action.partial.vue +30 -0
- package/src/components/partials/client-only.partial.vue +11 -0
- package/src/components/partials/error.partial.vue +73 -0
- package/src/components/partials/main-components.partial.vue +167 -0
- package/src/components/partials/mediadetection.partial.vue +71 -0
- package/src/components/partials/qrcode.partial.vue +35 -0
- package/src/customs.ts +12 -0
- package/src/env.d.ts +1 -0
- package/src/functions/PaymenyProviderFactory.ts +27 -0
- package/src/functions/lila-dom.ts +100 -0
- package/src/functions/shopify.provider.ts +378 -0
- package/src/functions/stripe.provider.ts +181 -0
- package/src/globals.d.ts +19 -0
- package/src/index.ts +47 -0
- package/src/interfaces/AppState.interface.ts +4 -0
- package/src/interfaces/EditorConfiguration.interface.ts +15 -0
- package/src/interfaces/EventDeclaration.interface.ts +19 -0
- package/src/interfaces/FrontendConfig.interface.ts +42 -0
- package/src/interfaces/GenericEvents.interface.ts +6 -0
- package/src/interfaces/GenericState.interface.ts +5 -0
- package/src/interfaces/IconsPartial.ts +9 -0
- package/src/interfaces/IdTokenExtended.interface.ts +7 -0
- package/src/interfaces/ModuleBaseProps.interface.ts +8 -0
- package/src/interfaces/PaymentProvider.interface.ts +13 -0
- package/src/libs/ActionNotice.ts +235 -0
- package/src/libs/Models.class.ts +482 -0
- package/src/main.ts +84 -0
- package/src/mixins/createCookieString.ts +78 -0
- package/src/mixins/createModelDeclaration.ts +29 -0
- package/src/mixins/createRouter.ts +30 -0
- package/src/mixins/date.ts +23 -0
- package/src/mixins/formatSize.ts +12 -0
- package/src/mixins/getAnchor.ts +14 -0
- package/src/mixins/getRoutes.ts +50 -0
- package/src/mixins/hasSlotContent.ts +32 -0
- package/src/mixins/hooks.ts +104 -0
- package/src/mixins/loadComponents.ts +107 -0
- package/src/mixins/logger.ts +32 -0
- package/src/mixins/replaceVariables.ts +19 -0
- package/src/mixins/scroll.ts +83 -0
- package/src/models/Address.model.ts +24 -0
- package/src/models/Contact.model.ts +22 -0
- package/src/models.ts +2 -0
- package/src/plugins/auth.ts +121 -0
- package/src/plugins/currency.ts +26 -0
- package/src/plugins/events.ts +156 -0
- package/src/plugins/filters.ts +43 -0
- package/src/plugins/inview.ts +351 -0
- package/src/plugins/replacer.ts +18 -0
- package/src/plugins/resize.ts +128 -0
- package/src/plugins/signupFlow.ts +89 -0
- package/src/plugins/traceable.ts +53 -0
- package/src/plugins/translations.ts +29 -0
- package/src/plugins/youtube.ts +80 -0
- package/src/routes.ts +132 -0
- package/src/server-entry.ts +113 -0
- package/src/stores/calls.store.ts +33 -0
- package/src/stores/cart.store.ts +186 -0
- package/src/stores/content.store.ts +83 -0
- package/src/stores/editor.store.ts +27 -0
- package/src/stores/files.store.ts +166 -0
- package/src/stores/main.store.ts +184 -0
- package/src/stores/user.store.ts +124 -0
- package/src/translations/de.ts +138 -0
- package/src/views/content.view.vue +219 -0
- package/src/views/download.view.vue +143 -0
- package/src/views/editor.view.vue +243 -0
- package/src/views/login.view.vue +82 -0
- package/src/views/signup-account.view.vue +64 -0
- package/tooling/cypress/config.ts +80 -0
- package/tooling/cypress/generate-manifest.js +5 -0
- package/tooling/cypress/manifest-utils.mjs +40 -0
- package/tooling/cypress/run-parallel.mjs +77 -0
- package/tooling/cypress/support/commands.ts +436 -0
- package/tooling/cypress/support/e2e.ts +17 -0
- package/tooling/eslint.config.js +223 -0
- package/tooling/preview.plugin.ts +134 -0
- package/tooling/ssr-test/index.mjs +391 -0
- package/tooling/stylelint.config.mjs +24 -0
- package/tooling/vite.ts +246 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import useEditorStore from '../stores/editor.store';
|
|
3
|
+
import { hardCopy, prepareContent } from '@lilaquadrat/studio/lib/esm/frontend';
|
|
4
|
+
import { getCurrentInstance, nextTick, onMounted, ref } from 'vue';
|
|
5
|
+
import { type Content, type ContentWithPositions, type EditorActiveModule, type StudioIframeMessage } from '@lilaquadrat/interfaces';
|
|
6
|
+
import { onBeforeUnmount } from 'vue';
|
|
7
|
+
import { watch } from 'vue';
|
|
8
|
+
import type { AppEditorConfiguration } from '@lilaquadrat/interfaces';
|
|
9
|
+
import { loadViaDeclarationSync } from '../mixins/loadComponents';
|
|
10
|
+
import useContentStore from '../stores/content.store';
|
|
11
|
+
import useMainStore from '../stores/main.store';
|
|
12
|
+
import { useEvents } from '../plugins/events';
|
|
13
|
+
import { scrollToSelector } from '../mixins/scroll';
|
|
14
|
+
|
|
15
|
+
const { eventDeclarations} = useEvents();
|
|
16
|
+
const currentInstance = getCurrentInstance();
|
|
17
|
+
const editorStore = useEditorStore();
|
|
18
|
+
const contentStore = useContentStore();
|
|
19
|
+
const content = ref<ContentWithPositions>({ settings: {}, top: [], content: [], bottom: [], additional: {} });
|
|
20
|
+
const siteSettings = ref<Omit<Content, 'modules'|'genericData'|'childData'>>();
|
|
21
|
+
const live = ref<boolean>(false);
|
|
22
|
+
// const parentUrl = ref<string>(`${window.location.protocol}//${window.location.host}/editor`);
|
|
23
|
+
const contentCache = ref<Content['modules']>();
|
|
24
|
+
const settingsCache = ref<AppEditorConfiguration>();
|
|
25
|
+
const active = ref<EditorActiveModule>();
|
|
26
|
+
const mainStore = useMainStore();
|
|
27
|
+
const init = ref<boolean>(false);
|
|
28
|
+
|
|
29
|
+
watch(siteSettings, () => {
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* if the target type changes we need to update the available modules
|
|
33
|
+
*/
|
|
34
|
+
const useTarget = siteSettings.value?.target || 'browser';
|
|
35
|
+
|
|
36
|
+
if(useTarget !== mainStore.target) {
|
|
37
|
+
|
|
38
|
+
const target = siteSettings.value?.target === 'browser' || !siteSettings.value?.target
|
|
39
|
+
? 'browser'
|
|
40
|
+
: siteSettings.value?.target
|
|
41
|
+
|
|
42
|
+
mainStore.target = useTarget;
|
|
43
|
+
|
|
44
|
+
postModules();
|
|
45
|
+
postCustomModules();
|
|
46
|
+
|
|
47
|
+
if (currentInstance) {
|
|
48
|
+
|
|
49
|
+
loadViaDeclarationSync(target === 'browser' ? editorStore.modulesBrowser : editorStore.modulesMail, 'lila', 'module', currentInstance.appContext.app);
|
|
50
|
+
loadViaDeclarationSync(target === 'browser' ? editorStore.partialsBrowser : editorStore.partialsMail, 'lila', 'partial', currentInstance.appContext.app);
|
|
51
|
+
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
function messageHandler (message: StudioIframeMessage) {
|
|
59
|
+
|
|
60
|
+
if (message.data.type === 'studio-content') {
|
|
61
|
+
console.groupCollapsed('MESSAGE_STUDIO_CONTENT');
|
|
62
|
+
console.debug(message.data.data);
|
|
63
|
+
console.groupEnd();
|
|
64
|
+
|
|
65
|
+
contentCache.value = message.data.data;
|
|
66
|
+
updateContent();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
//editor main configuration
|
|
70
|
+
if (message.data.type === 'studio-settings') {
|
|
71
|
+
console.groupCollapsed('MESSAGE_STUDIO_SETTINGS');
|
|
72
|
+
console.debug(message.data.data);
|
|
73
|
+
console.groupEnd();
|
|
74
|
+
|
|
75
|
+
settingsCache.value = message.data.data;
|
|
76
|
+
mainStore.configuration = message.data.data;
|
|
77
|
+
updateContent();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (message.data.type === 'studio-editor-settings') {
|
|
81
|
+
console.groupCollapsed('MESSAGE_STUDIO_EDITOR_SETTINGS');
|
|
82
|
+
console.debug(message.data.data);
|
|
83
|
+
console.groupEnd();
|
|
84
|
+
|
|
85
|
+
siteSettings.value = message.data.data;
|
|
86
|
+
|
|
87
|
+
if(!init.value) {
|
|
88
|
+
|
|
89
|
+
postModules();
|
|
90
|
+
postCustomModules();
|
|
91
|
+
postEvents();
|
|
92
|
+
init.value = true;
|
|
93
|
+
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
updateContent();
|
|
97
|
+
updateContext();
|
|
98
|
+
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (message.data.type === 'studio-active') {
|
|
102
|
+
console.groupCollapsed('MESSAGE_STUDIO_ACTIVE');
|
|
103
|
+
console.debug(message.data.data);
|
|
104
|
+
console.groupEnd();
|
|
105
|
+
|
|
106
|
+
active.value = message.data.data;
|
|
107
|
+
|
|
108
|
+
if (active.value) {
|
|
109
|
+
|
|
110
|
+
scrollToModule(active.value);
|
|
111
|
+
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (message.data.type === 'studio-cookie-reset') {
|
|
116
|
+
console.groupCollapsed('MESSAGE_STUDIO_COOKIE_RESET');
|
|
117
|
+
console.debug(message.data.data);
|
|
118
|
+
console.groupEnd();
|
|
119
|
+
|
|
120
|
+
resetCookies();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
onMounted(() => {
|
|
125
|
+
|
|
126
|
+
if (window.origin === 'null') {
|
|
127
|
+
live.value = true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (live.value) return;
|
|
131
|
+
|
|
132
|
+
removeListeners()
|
|
133
|
+
addListeners();
|
|
134
|
+
|
|
135
|
+
window.parent.postMessage('studio-design-settings', '*');
|
|
136
|
+
window.parent.postMessage('studio-design-ready', '*');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
onBeforeUnmount(() => removeListeners());
|
|
140
|
+
|
|
141
|
+
function removeListeners () {
|
|
142
|
+
window.removeEventListener('message', messageHandler, false);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function addListeners () {
|
|
146
|
+
window.addEventListener('message', messageHandler, false);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function updateContent () {
|
|
150
|
+
content.value = prepareContent({ modules: contentCache.value, ...siteSettings.value });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function postModules () {
|
|
154
|
+
|
|
155
|
+
console.log('POST MODULES', mainStore.target);
|
|
156
|
+
|
|
157
|
+
const modules = mainStore.target === 'browser'
|
|
158
|
+
? editorStore.availableModulesWithRevision
|
|
159
|
+
: editorStore.availableModulesWithRevisionMail
|
|
160
|
+
|
|
161
|
+
window.parent.postMessage(
|
|
162
|
+
{
|
|
163
|
+
type: 'studio-design-modules-with-revision',
|
|
164
|
+
data: hardCopy(modules),
|
|
165
|
+
},
|
|
166
|
+
'*',
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function postCustomModules () {
|
|
172
|
+
|
|
173
|
+
const modules = mainStore.target === 'browser'
|
|
174
|
+
? mainStore.customModulesBrowser
|
|
175
|
+
: mainStore.customModulesMail
|
|
176
|
+
|
|
177
|
+
if (!modules) return;
|
|
178
|
+
|
|
179
|
+
window.parent.postMessage(
|
|
180
|
+
{
|
|
181
|
+
type: 'studio-design-custom-modules',
|
|
182
|
+
data: hardCopy(modules),
|
|
183
|
+
},
|
|
184
|
+
'*',
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function postEvents () {
|
|
190
|
+
|
|
191
|
+
window.parent.postMessage(
|
|
192
|
+
{
|
|
193
|
+
type: 'studio-design-events',
|
|
194
|
+
data: eventDeclarations,
|
|
195
|
+
},
|
|
196
|
+
'*',
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* sets sitetitle and description
|
|
203
|
+
*/
|
|
204
|
+
function updateContext () {
|
|
205
|
+
|
|
206
|
+
contentStore.setContext({
|
|
207
|
+
sitetitle : siteSettings?.value?.settings?.title,
|
|
208
|
+
description: siteSettings?.value?.settings?.description,
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function resetCookies () {
|
|
214
|
+
const cookies = document.cookie.split(';').filter((single) => single);
|
|
215
|
+
|
|
216
|
+
cookies.forEach((single) => {
|
|
217
|
+
const name = single.split('=');
|
|
218
|
+
|
|
219
|
+
document.cookie = `${name[0].trim()}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=None;`;
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function scrollToModule (active: EditorActiveModule): void {
|
|
224
|
+
|
|
225
|
+
nextTick().then(() => {
|
|
226
|
+
const baseModule = contentCache.value?.find((single) => single.uuid === active.uuid);
|
|
227
|
+
const containerSelector = baseModule?.position === 'content' || !baseModule?.position ? '.lila-content-module .container:not(.top, .bottom)' : `.lila-content-module .container.${baseModule.position}`;
|
|
228
|
+
const contentPosition: Content['modules'] = baseModule?.position === 'content' || !baseModule?.position ? content.value.content : content.value[baseModule.position];
|
|
229
|
+
const index = contentPosition?.findIndex((single) => single.uuid === active.uuid);
|
|
230
|
+
|
|
231
|
+
if (!index && index !== 0) return;
|
|
232
|
+
|
|
233
|
+
scrollToSelector(`${containerSelector} .lila-module:nth-child(${index + 1}), ${containerSelector} .partial-container:nth-child(${index + 1})`,)
|
|
234
|
+
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
}
|
|
238
|
+
</script>
|
|
239
|
+
<template>
|
|
240
|
+
<article class="editor-screen screen">
|
|
241
|
+
<lila-content-module :content="content" />
|
|
242
|
+
</article>
|
|
243
|
+
</template>
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type AppState from '../interfaces/AppState.interface';
|
|
3
|
+
import { auth } from '../plugins/auth';
|
|
4
|
+
import useMainStore from '../stores/main.store';
|
|
5
|
+
import useUserStore from '../stores/user.store';
|
|
6
|
+
import type { RedirectLoginResult } from '@auth0/auth0-spa-js';
|
|
7
|
+
import StudioSDK from '@lilaquadrat/sdk';
|
|
8
|
+
import { onBeforeMount, ref } from 'vue';
|
|
9
|
+
import { useRouter } from 'vue-router';
|
|
10
|
+
import { routeForLock } from '../plugins/signupFlow';
|
|
11
|
+
|
|
12
|
+
const mainStore = useMainStore();
|
|
13
|
+
const userStore = useUserStore();
|
|
14
|
+
const router = useRouter();
|
|
15
|
+
const error = ref<boolean>(false);
|
|
16
|
+
const hint = ref<string>();
|
|
17
|
+
|
|
18
|
+
onBeforeMount(async () => {
|
|
19
|
+
|
|
20
|
+
let loginResult: RedirectLoginResult<AppState>;
|
|
21
|
+
|
|
22
|
+
// Attempt to handle the authentication callback.
|
|
23
|
+
try {
|
|
24
|
+
|
|
25
|
+
loginResult = await auth.handleCallback();
|
|
26
|
+
|
|
27
|
+
} catch (e: any) {
|
|
28
|
+
|
|
29
|
+
console.error(e);
|
|
30
|
+
error.value = true;
|
|
31
|
+
return;
|
|
32
|
+
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
await userStore.updateLock();
|
|
36
|
+
|
|
37
|
+
console.log(userStore.locked, loginResult);
|
|
38
|
+
|
|
39
|
+
const sdk = new StudioSDK(mainStore.apiConfig);
|
|
40
|
+
|
|
41
|
+
if(userStore.locked === 'user-connect' && loginResult?.appState?.customerId) {
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
|
|
45
|
+
// Attempt to connect the user with the provided customerId.
|
|
46
|
+
await sdk.members.me.connect(loginResult?.appState?.customerId);
|
|
47
|
+
await userStore.updateLock();
|
|
48
|
+
|
|
49
|
+
} catch (e) {
|
|
50
|
+
|
|
51
|
+
console.error(e);
|
|
52
|
+
console.error('CONNECT_ACCOUNT_TO_CUSTOMER_FAILED');
|
|
53
|
+
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if(!userStore.userData?.emailConfirmed && loginResult?.appState?.emailConfirmationCode) {
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
|
|
62
|
+
// Attempt to confirm the email address with the provided code
|
|
63
|
+
await sdk.members.me.confirmEmail(loginResult?.appState?.emailConfirmationCode)
|
|
64
|
+
await userStore.updateLock();
|
|
65
|
+
|
|
66
|
+
} catch (e) {
|
|
67
|
+
|
|
68
|
+
console.error(e);
|
|
69
|
+
console.error('EMAIL_CONFIRMATION_FAILED');
|
|
70
|
+
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
router.replace(routeForLock(userStore.locked));
|
|
76
|
+
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
</script>
|
|
80
|
+
<template>
|
|
81
|
+
<lila-error-partial v-if="error" :status="400" :hint="hint" type="members" />
|
|
82
|
+
</template>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { auth } from '../plugins/auth';
|
|
3
|
+
import useMainStore from '../stores/main.store';
|
|
4
|
+
import useUserStore from '../stores/user.store';
|
|
5
|
+
import { prepareContent } from '@lilaquadrat/studio/lib/esm/frontend';
|
|
6
|
+
import type { ContentWithPositions } from '@lilaquadrat/interfaces';
|
|
7
|
+
import { computed, onBeforeMount } from 'vue';
|
|
8
|
+
import { useRoute, useRouter } from 'vue-router';
|
|
9
|
+
|
|
10
|
+
const route = useRoute();
|
|
11
|
+
const router = useRouter();
|
|
12
|
+
const userStore = useUserStore();
|
|
13
|
+
const store = useMainStore();
|
|
14
|
+
|
|
15
|
+
onBeforeMount(() => {
|
|
16
|
+
|
|
17
|
+
if(route.query.customerId) {
|
|
18
|
+
|
|
19
|
+
userStore.setCustomer({ _id: route.query.customerId as string });
|
|
20
|
+
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if(route.query.emailConfirmationCode) {
|
|
24
|
+
|
|
25
|
+
userStore.emailConfirmationCode = route.query.emailConfirmationCode as string;
|
|
26
|
+
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if(!auth.isAuth.value) {
|
|
30
|
+
|
|
31
|
+
auth.triggerSignup(userStore.customer?._id, userStore.emailConfirmationCode);
|
|
32
|
+
|
|
33
|
+
} else {
|
|
34
|
+
|
|
35
|
+
router.push({ name: 'members' });
|
|
36
|
+
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const contentMerged = computed(() => {
|
|
42
|
+
|
|
43
|
+
const data = store.staticData?.redirect;
|
|
44
|
+
|
|
45
|
+
if(data) return prepareContent(data) as unknown as ContentWithPositions;
|
|
46
|
+
return prepareContent({}) as unknown as ContentWithPositions;
|
|
47
|
+
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
</script>
|
|
51
|
+
<template>
|
|
52
|
+
<section class="signup-account-view">
|
|
53
|
+
<lila-content-module v-if="contentMerged" :content="contentMerged" />
|
|
54
|
+
</section>
|
|
55
|
+
</template>
|
|
56
|
+
<style lang="less" scoped>
|
|
57
|
+
.signup-account-view {
|
|
58
|
+
display: grid;
|
|
59
|
+
align-content: center;
|
|
60
|
+
justify-content: center;
|
|
61
|
+
|
|
62
|
+
height: 80vh;
|
|
63
|
+
}
|
|
64
|
+
</style>
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url'
|
|
2
|
+
import { defineConfig } from 'cypress'
|
|
3
|
+
import { rebuildManifest, writeManifest, screenshotToManifestPath } from './manifest-utils.mjs'
|
|
4
|
+
|
|
5
|
+
type CypressConfig = Parameters<typeof defineConfig>[0];
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* the screenshot harness: seeds cypress/manifest.js from the existing
|
|
9
|
+
* screenshots, appends every new one as it is taken and pins the browser window
|
|
10
|
+
* so a run on another machine produces comparable images.
|
|
11
|
+
*
|
|
12
|
+
* pass overrides for anything project specific (baseUrl, viewport, extra
|
|
13
|
+
* setupNodeEvents).
|
|
14
|
+
*/
|
|
15
|
+
export function createCypressConfig (overrides: CypressConfig = {}) {
|
|
16
|
+
|
|
17
|
+
const { e2e: e2eOverrides, ...rest } = overrides;
|
|
18
|
+
|
|
19
|
+
return defineConfig({
|
|
20
|
+
e2e: {
|
|
21
|
+
baseUrl : 'http://localhost:5173/test',
|
|
22
|
+
/**
|
|
23
|
+
* the shared commands live in the package, so a project does not need a
|
|
24
|
+
* cypress/support folder of its own
|
|
25
|
+
*/
|
|
26
|
+
supportFile : fileURLToPath(new URL('./support/e2e.ts', import.meta.url)),
|
|
27
|
+
viewportHeight: 1920,
|
|
28
|
+
viewportWidth : 1080,
|
|
29
|
+
video : false,
|
|
30
|
+
testIsolation : false,
|
|
31
|
+
...e2eOverrides,
|
|
32
|
+
/**
|
|
33
|
+
* always the framework handler: it chains into an override at the end, so
|
|
34
|
+
* a project can add its own tasks without losing the manifest wiring
|
|
35
|
+
*/
|
|
36
|
+
setupNodeEvents (on, config) {
|
|
37
|
+
const root = process.cwd();
|
|
38
|
+
const files = rebuildManifest(root);
|
|
39
|
+
|
|
40
|
+
console.log(`[manifest] seeded with ${files.length} existing screenshots`);
|
|
41
|
+
|
|
42
|
+
on('after:screenshot', (details) => {
|
|
43
|
+
files.push(screenshotToManifestPath(root, details.path));
|
|
44
|
+
writeManifest(root, files);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
on('before:browser:launch', (browser, launchOptions) => {
|
|
48
|
+
if (browser.name === 'electron') {
|
|
49
|
+
launchOptions.preferences.width = 1920
|
|
50
|
+
launchOptions.preferences.height = 3000
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (browser.family === 'chromium' && browser.name !== 'electron') {
|
|
54
|
+
launchOptions.args.push('--window-size=1920,1080');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (browser.name === 'firefox') {
|
|
58
|
+
launchOptions.args.push('--width=1920')
|
|
59
|
+
launchOptions.args.push('--height=3000')
|
|
60
|
+
|
|
61
|
+
launchOptions.preferences['layout.css.devPixelsPerPx'] = '1.0'
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (browser.name === 'chrome' && browser.isHeadless) {
|
|
65
|
+
launchOptions.args.push('--window-size=1920,3000');
|
|
66
|
+
launchOptions.args.push('--force-device-scale-factor=1');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return launchOptions
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
return e2eOverrides?.setupNodeEvents?.(on, config);
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
...rest,
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export default createCypressConfig;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { writeFileSync, readdirSync, statSync, existsSync } from 'fs';
|
|
2
|
+
import { resolve, join, relative } from 'path';
|
|
3
|
+
|
|
4
|
+
export function walkImages (dir, base = dir) {
|
|
5
|
+
return readdirSync(dir).flatMap((entry) => {
|
|
6
|
+
const full = join(dir, entry);
|
|
7
|
+
|
|
8
|
+
if (statSync(full).isDirectory()) return walkImages(full, base);
|
|
9
|
+
if (/\.(png|jpe?g)$/i.test(entry)) return [full.slice(base.length + 1).replace(/\\/g, '/')];
|
|
10
|
+
return [];
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function writeManifest (root, files) {
|
|
15
|
+
const content = `window.__SCREENSHOTS__ = ${JSON.stringify(files.slice().sort(), null, 2)};\n`;
|
|
16
|
+
|
|
17
|
+
writeFileSync(resolve(root, 'cypress/manifest.js'), content, 'utf-8');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function scanScreenshots (root) {
|
|
21
|
+
const screenshotsDir = resolve(root, 'cypress/screenshots');
|
|
22
|
+
|
|
23
|
+
if (!existsSync(screenshotsDir)) return [];
|
|
24
|
+
|
|
25
|
+
return walkImages(screenshotsDir).map((f) => `screenshots/${f}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function rebuildManifest (root = process.cwd()) {
|
|
29
|
+
const files = scanScreenshots(root);
|
|
30
|
+
|
|
31
|
+
writeManifest(root, files);
|
|
32
|
+
|
|
33
|
+
return files;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function screenshotToManifestPath (root, absolutePath) {
|
|
37
|
+
const screenshotsDir = resolve(root, 'cypress/screenshots');
|
|
38
|
+
|
|
39
|
+
return `screenshots/${relative(screenshotsDir, absolutePath).replace(/\\/g, '/')}`;
|
|
40
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { readdirSync, existsSync, readFileSync } from 'fs';
|
|
3
|
+
import { resolve } from 'path';
|
|
4
|
+
|
|
5
|
+
// Measured single-run durations (seconds) for the base design specs. A project
|
|
6
|
+
// with its own spec set overrides or extends these through cypress/durations.json.
|
|
7
|
+
// Measured single-run durations (seconds) — used to bin-pack shards evenly instead
|
|
8
|
+
// of a blind round-robin, since a few specs (picture, picturegroup, timeline, emotion,
|
|
9
|
+
// gallery) run 3-6x longer than the rest and would otherwise dominate one shard.
|
|
10
|
+
// Unlisted specs (new ones added later) fall back to the average below.
|
|
11
|
+
const KNOWN_DURATIONS = {
|
|
12
|
+
'blog-intro.cy.ts' : 41,
|
|
13
|
+
'event.cy.ts' : 25,
|
|
14
|
+
'gallery.cy.ts' : 194,
|
|
15
|
+
'navigation.cy.ts' : 64,
|
|
16
|
+
'prices.cy.ts' : 19,
|
|
17
|
+
'contact.cy.ts' : 94,
|
|
18
|
+
'facts.cy.ts' : 103,
|
|
19
|
+
'location.cy.ts' : 3,
|
|
20
|
+
'pictureandtext.cy.ts': 152,
|
|
21
|
+
'text.cy.ts' : 29,
|
|
22
|
+
'compare.cy.ts' : 39,
|
|
23
|
+
'eventlist.cy.ts' : 44,
|
|
24
|
+
'index.cy.ts' : 79,
|
|
25
|
+
'picture.cy.ts' : 376,
|
|
26
|
+
'quote.cy.ts' : 40,
|
|
27
|
+
'FAQ.cy.ts' : 95,
|
|
28
|
+
'emotion.cy.ts' : 197,
|
|
29
|
+
'footer.cy.ts' : 26,
|
|
30
|
+
'menu.cy.ts' : 31,
|
|
31
|
+
'picturegroup.cy.ts' : 300,
|
|
32
|
+
'timeline.cy.ts' : 256,
|
|
33
|
+
};
|
|
34
|
+
const root = process.cwd();
|
|
35
|
+
const durationsFile = resolve(root, 'cypress/durations.json');
|
|
36
|
+
const DURATIONS = existsSync(durationsFile)
|
|
37
|
+
? { ...KNOWN_DURATIONS, ...JSON.parse(readFileSync(durationsFile, 'utf-8')) }
|
|
38
|
+
: KNOWN_DURATIONS;
|
|
39
|
+
const AVG_DURATION = Object.values(DURATIONS).reduce((a, b) => a + b, 0) / Object.keys(DURATIONS).length;
|
|
40
|
+
const cypressBin = resolve(root, 'node_modules/.bin/cypress');
|
|
41
|
+
const specsDir = resolve(root, 'cypress/e2e');
|
|
42
|
+
const specNames = readdirSync(specsDir).filter((f) => f.endsWith('.cy.ts'));
|
|
43
|
+
const shardCount = Math.max(1, Math.min(Number(process.env.CY_SHARDS) || 4, specNames.length));
|
|
44
|
+
const shards = Array.from({ length: shardCount }, () => ({ specs: [], total: 0 }));
|
|
45
|
+
|
|
46
|
+
// Longest-processing-time-first: sort heaviest specs first, always drop the next
|
|
47
|
+
// spec into whichever shard currently has the least total weight.
|
|
48
|
+
specNames
|
|
49
|
+
.map((name) => ({ name, duration: DURATIONS[name] ?? AVG_DURATION }))
|
|
50
|
+
.sort((a, b) => b.duration - a.duration)
|
|
51
|
+
.forEach(({ name, duration }) => {
|
|
52
|
+
const lightest = shards.reduce((min, s) => (s.total < min.total ? s : min), shards[0]);
|
|
53
|
+
|
|
54
|
+
lightest.specs.push(`cypress/e2e/${name}`);
|
|
55
|
+
lightest.total += duration;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
console.log(`[parallel] running ${specNames.length} specs across ${shardCount} shards (est. longest shard ~${Math.round(Math.max(...shards.map((s) => s.total)) / 60)}m)`);
|
|
59
|
+
|
|
60
|
+
// Each shard writes its own partial cypress/manifest.js as it goes (from its own
|
|
61
|
+
// in-process view of the screenshots dir), so the manifest is only trustworthy once
|
|
62
|
+
// every shard has finished — the "cy:parallel" script always follows this up with
|
|
63
|
+
// a single authoritative `generate-manifest.js` rebuild over the full directory.
|
|
64
|
+
const runs = shards.map(({ specs }, i) => new Promise((resolveRun, rejectRun) => {
|
|
65
|
+
const child = spawn(cypressBin, ['run', '--spec', specs.join(',')], { stdio: 'inherit' });
|
|
66
|
+
|
|
67
|
+
child.on('exit', (code) => {
|
|
68
|
+
if (code === 0) resolveRun();
|
|
69
|
+
else rejectRun(new Error(`shard ${i} failed (exit code ${code})`));
|
|
70
|
+
});
|
|
71
|
+
}));
|
|
72
|
+
const results = await Promise.allSettled(runs);
|
|
73
|
+
const failed = results.filter((r) => r.status === 'rejected');
|
|
74
|
+
|
|
75
|
+
failed.forEach((r) => console.error(`[parallel] ${r.reason.message}`));
|
|
76
|
+
|
|
77
|
+
if (failed.length > 0) process.exit(1);
|