@vue-skuilder/platform-ui 0.1.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.
- package/LICENCE +661 -0
- package/README.md +64 -0
- package/dist/assets/Roboto-Black-B0ZKieaB.woff +0 -0
- package/dist/assets/Roboto-Black-VhoA2qKx.woff2 +0 -0
- package/dist/assets/Roboto-BlackItalic-D0gSnuIb.woff +0 -0
- package/dist/assets/Roboto-BlackItalic-D4yie1YO.woff2 +0 -0
- package/dist/assets/Roboto-Bold-D9plYbeK.woff +0 -0
- package/dist/assets/Roboto-Bold-hN3duQhD.woff2 +0 -0
- package/dist/assets/Roboto-BoldItalic-BWDm51uc.woff2 +0 -0
- package/dist/assets/Roboto-BoldItalic-CyLKvOHD.woff +0 -0
- package/dist/assets/Roboto-Light-Cu-PAxXt.woff +0 -0
- package/dist/assets/Roboto-Light-DHTugVNA.woff2 +0 -0
- package/dist/assets/Roboto-LightItalic-CZg5kHIB.woff +0 -0
- package/dist/assets/Roboto-LightItalic-JQyp2Y3P.woff2 +0 -0
- package/dist/assets/Roboto-Medium-ByKogCTi.woff2 +0 -0
- package/dist/assets/Roboto-Medium-b81vv18W.woff +0 -0
- package/dist/assets/Roboto-MediumItalic-DFQ-RYa0.woff +0 -0
- package/dist/assets/Roboto-MediumItalic-i1eR0KbF.woff2 +0 -0
- package/dist/assets/Roboto-Regular-BX5l9hRW.woff +0 -0
- package/dist/assets/Roboto-Regular-C6rbFxYz.woff2 +0 -0
- package/dist/assets/Roboto-RegularItalic-BjnLZsam.woff +0 -0
- package/dist/assets/Roboto-RegularItalic-CvPUdkvM.woff2 +0 -0
- package/dist/assets/Roboto-Thin-BfJvJcog.woff +0 -0
- package/dist/assets/Roboto-Thin-NicBC1pN.woff2 +0 -0
- package/dist/assets/Roboto-ThinItalic-CKlCjrO_.woff2 +0 -0
- package/dist/assets/Roboto-ThinItalic-DnIWFxRE.woff +0 -0
- package/dist/assets/index-CQ-sNKGW.css +14 -0
- package/dist/assets/index-EbqpUgvM.js +161 -0
- package/dist/assets/materialdesignicons-webfont-B7mPwVP_.ttf +0 -0
- package/dist/assets/materialdesignicons-webfont-CSr8KVlo.eot +0 -0
- package/dist/assets/materialdesignicons-webfont-Dp5v-WZN.woff2 +0 -0
- package/dist/assets/materialdesignicons-webfont-PXm3-2wK.woff +0 -0
- package/dist/assets/workbox-window.prod.es5-p40uij6f.js +1 -0
- package/dist/favicon.ico +0 -0
- package/dist/img/icons/safari-pinned-tab.svg +149 -0
- package/dist/index.html +19 -0
- package/dist/manifest.json +20 -0
- package/dist/manifest.webmanifest +1 -0
- package/dist/robots.txt +2 -0
- package/dist/sw.js +1 -0
- package/dist/workbox-1be04862.js +1 -0
- package/package.json +105 -0
- package/src/App.vue +156 -0
- package/src/ENVIRONMENT_VARS.ts +79 -0
- package/src/components/Classrooms/ClassroomCtrlPanel.vue +206 -0
- package/src/components/Classrooms/CreateClassroom.vue +159 -0
- package/src/components/Classrooms/JoinCode.vue +83 -0
- package/src/components/Courses/CourseCardBrowser.vue +365 -0
- package/src/components/Courses/CourseEditor.vue +164 -0
- package/src/components/Courses/CourseInformation.vue +164 -0
- package/src/components/Courses/CourseRouter.vue +116 -0
- package/src/components/Courses/CourseStubCard.vue +76 -0
- package/src/components/Courses/EloModeration.vue +122 -0
- package/src/components/Courses/TagInformation.vue +209 -0
- package/src/components/Edit/BulkImport/CardPreviewList.vue +345 -0
- package/src/components/Edit/BulkImportView.vue +633 -0
- package/src/components/Edit/CardBrowser.vue +79 -0
- package/src/components/Edit/ComponentRegistration/ComponentRegistration.vue +235 -0
- package/src/components/Edit/ComponentRegistration/UnregisteredComponentsTable.vue +19 -0
- package/src/components/Edit/CourseEditor.vue +162 -0
- package/src/components/Edit/NavigationStrategy/NavigationStrategyEditor.vue +170 -0
- package/src/components/Edit/NavigationStrategy/NavigationStrategyList.vue +92 -0
- package/src/components/Edit/TagsInput.vue +247 -0
- package/src/components/Edit/ViewableDataInputForm/DataInputForm.vue +524 -0
- package/src/components/Edit/ViewableDataInputForm/FieldInput.types.ts +33 -0
- package/src/components/Edit/ViewableDataInputForm/FieldInputs/AudioInput.vue +188 -0
- package/src/components/Edit/ViewableDataInputForm/FieldInputs/ChessPuzzleInput.vue +79 -0
- package/src/components/Edit/ViewableDataInputForm/FieldInputs/FieldInput.css +12 -0
- package/src/components/Edit/ViewableDataInputForm/FieldInputs/ImageInput.vue +231 -0
- package/src/components/Edit/ViewableDataInputForm/FieldInputs/IntegerInput.vue +49 -0
- package/src/components/Edit/ViewableDataInputForm/FieldInputs/MarkdownInput.vue +34 -0
- package/src/components/Edit/ViewableDataInputForm/FieldInputs/MediaDragDropUploader.vue +246 -0
- package/src/components/Edit/ViewableDataInputForm/FieldInputs/MidiInput.vue +113 -0
- package/src/components/Edit/ViewableDataInputForm/FieldInputs/NumberInput.vue +49 -0
- package/src/components/Edit/ViewableDataInputForm/FieldInputs/StringInput.vue +49 -0
- package/src/components/Edit/ViewableDataInputForm/FieldInputs/typeValidators.ts +49 -0
- package/src/components/Edit/ViewableDataInputForm/OptionsFieldInput.ts +161 -0
- package/src/components/Study/SessionConfiguration.vue +371 -0
- package/src/components/TextSwap.vue +65 -0
- package/src/components/User/UserStats.vue +30 -0
- package/src/dev/DataInputFormTester.vue +117 -0
- package/src/dev/readme.md +3 -0
- package/src/enums.ts +0 -0
- package/src/glyphs.txt +933 -0
- package/src/main.ts +45 -0
- package/src/plugins/vuetify.ts +41 -0
- package/src/registerServiceWorker.ts +18 -0
- package/src/router.ts +184 -0
- package/src/server/index.spec.ts +192 -0
- package/src/server/index.ts +71 -0
- package/src/shims-vue.d.ts +5 -0
- package/src/store.mock.ts +122 -0
- package/src/stores/useDataInputFormStore.ts +49 -0
- package/src/stores/useFieldInputStore.ts +191 -0
- package/src/types/shims-vuetify.d.ts +12 -0
- package/src/types/svg.d.ts +4 -0
- package/src/utils/bulkImport/index.ts +94 -0
- package/src/views/About.vue +29 -0
- package/src/views/Admin.vue +128 -0
- package/src/views/Classrooms.vue +258 -0
- package/src/views/Courses.vue +265 -0
- package/src/views/Home.vue +154 -0
- package/src/views/Login.vue +75 -0
- package/src/views/ReleaseNotes.vue +20 -0
- package/src/views/SignUp.vue +32 -0
- package/src/views/Study.vue +261 -0
- package/src/views/User.vue +109 -0
- package/src/vite-env.d.ts +1 -0
package/src/main.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import ENV from './ENVIRONMENT_VARS';
|
|
2
|
+
|
|
3
|
+
import 'roboto-fontface/css/roboto/roboto-fontface.css';
|
|
4
|
+
import { createApp } from 'vue';
|
|
5
|
+
import App from './App.vue';
|
|
6
|
+
import './registerServiceWorker';
|
|
7
|
+
import router from './router';
|
|
8
|
+
import { createPinia } from 'pinia';
|
|
9
|
+
import vuetify from './plugins/vuetify';
|
|
10
|
+
// `courses` imports - keep style import for initial page rendering
|
|
11
|
+
import '@vue-skuilder/courses/style';
|
|
12
|
+
// `db` import and initialization
|
|
13
|
+
import { initializeDataLayer } from '@vue-skuilder/db';
|
|
14
|
+
|
|
15
|
+
(async () => {
|
|
16
|
+
await initializeDataLayer({
|
|
17
|
+
type: 'pouch',
|
|
18
|
+
options: {
|
|
19
|
+
COUCHDB_SERVER_PROTOCOL: ENV.COUCHDB_SERVER_PROTOCOL,
|
|
20
|
+
COUCHDB_SERVER_URL: ENV.COUCHDB_SERVER_URL,
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const pinia = createPinia();
|
|
25
|
+
const app = createApp(App);
|
|
26
|
+
|
|
27
|
+
// Dynamically import { allCourses }
|
|
28
|
+
const { allCourses: Courses } = await import('@vue-skuilder/courses');
|
|
29
|
+
|
|
30
|
+
// Register all view components globally
|
|
31
|
+
const viewComponents = Courses.allViewsRaw();
|
|
32
|
+
Object.entries(viewComponents).forEach(([name, component]) => {
|
|
33
|
+
app.component(name, component);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
app.use(router);
|
|
37
|
+
app.use(vuetify);
|
|
38
|
+
app.use(pinia);
|
|
39
|
+
|
|
40
|
+
// Dynamically import piniaPlugin
|
|
41
|
+
const { piniaPlugin } = await import('@vue-skuilder/common-ui');
|
|
42
|
+
app.use(piniaPlugin, { pinia });
|
|
43
|
+
|
|
44
|
+
app.mount('#app');
|
|
45
|
+
})();
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// src/plugins/vuetify.ts
|
|
2
|
+
import '@mdi/font/css/materialdesignicons.css';
|
|
3
|
+
import 'vuetify/styles';
|
|
4
|
+
import { createVuetify } from 'vuetify';
|
|
5
|
+
import * as components from 'vuetify/components';
|
|
6
|
+
import * as directives from 'vuetify/directives';
|
|
7
|
+
|
|
8
|
+
export default createVuetify({
|
|
9
|
+
components,
|
|
10
|
+
directives,
|
|
11
|
+
theme: {
|
|
12
|
+
defaultTheme: 'light',
|
|
13
|
+
themes: {
|
|
14
|
+
light: {
|
|
15
|
+
colors: {
|
|
16
|
+
primary: '#1976D2',
|
|
17
|
+
secondary: '#424242',
|
|
18
|
+
accent: '#82B1FF',
|
|
19
|
+
error: '#FF5252',
|
|
20
|
+
info: '#2196F3',
|
|
21
|
+
success: '#4CAF50',
|
|
22
|
+
warning: '#FFC107',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
dark: {
|
|
26
|
+
colors: {
|
|
27
|
+
primary: '#2196F3',
|
|
28
|
+
secondary: '#424242',
|
|
29
|
+
accent: '#FF4081',
|
|
30
|
+
error: '#FF5252',
|
|
31
|
+
info: '#2196F3',
|
|
32
|
+
success: '#4CAF50',
|
|
33
|
+
warning: '#FFC107',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
icons: {
|
|
39
|
+
defaultSet: 'mdi',
|
|
40
|
+
},
|
|
41
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { registerSW } from 'virtual:pwa-register';
|
|
2
|
+
|
|
3
|
+
if (import.meta.env.PROD) {
|
|
4
|
+
registerSW({
|
|
5
|
+
onNeedRefresh() {
|
|
6
|
+
console.log('New content is available; please refresh.');
|
|
7
|
+
},
|
|
8
|
+
onOfflineReady() {
|
|
9
|
+
console.log('Content has been cached for offline use.');
|
|
10
|
+
},
|
|
11
|
+
onRegistered() {
|
|
12
|
+
console.log('Service worker has been registered.');
|
|
13
|
+
},
|
|
14
|
+
onRegisterError(error) {
|
|
15
|
+
console.error('Error during service worker registration:', error);
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
}
|
package/src/router.ts
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { MarkdownRenderer } from '@vue-skuilder/common-ui';
|
|
2
|
+
import { createRouter, createWebHistory } from 'vue-router';
|
|
3
|
+
import ClassroomCtrlPanel from './components/Classrooms/ClassroomCtrlPanel.vue';
|
|
4
|
+
import JoinCode from './components/Classrooms/JoinCode.vue';
|
|
5
|
+
import CourseRouter from './components/Courses/CourseRouter.vue';
|
|
6
|
+
import ELOModerator from './components/Courses/EloModeration.vue';
|
|
7
|
+
import TagInformation from './components/Courses/TagInformation.vue';
|
|
8
|
+
import CourseEditor from './components/Edit/CourseEditor.vue';
|
|
9
|
+
import Stats from './components/User/UserStats.vue';
|
|
10
|
+
import About from './views/About.vue';
|
|
11
|
+
import Admin from './views/Admin.vue';
|
|
12
|
+
import Classrooms from './views/Classrooms.vue';
|
|
13
|
+
import Courses from './views/Courses.vue';
|
|
14
|
+
import Home from './views/Home.vue';
|
|
15
|
+
import Login from './views/Login.vue';
|
|
16
|
+
import ReleaseNotes from './views/ReleaseNotes.vue';
|
|
17
|
+
import SignUp from './views/SignUp.vue';
|
|
18
|
+
import Study from './views/Study.vue';
|
|
19
|
+
import User from './views/User.vue';
|
|
20
|
+
import DataInputFormTester from './dev/DataInputFormTester.vue';
|
|
21
|
+
|
|
22
|
+
const router = createRouter({
|
|
23
|
+
history: createWebHistory(),
|
|
24
|
+
// mode: 'history', // deprecated in Vue 3 / Vue Router 4
|
|
25
|
+
|
|
26
|
+
routes: [
|
|
27
|
+
// {
|
|
28
|
+
// path: '/debug/:component',
|
|
29
|
+
// name: 'componentPreviews',
|
|
30
|
+
// component: components[component]
|
|
31
|
+
// }
|
|
32
|
+
// todo:
|
|
33
|
+
//
|
|
34
|
+
// beforeEnter: () => authenticateAdmin ?
|
|
35
|
+
//
|
|
36
|
+
// const components: Component[] = [];
|
|
37
|
+
{
|
|
38
|
+
path: '/dif/:pathMatch(.*)',
|
|
39
|
+
name: 'testThePathComponent',
|
|
40
|
+
component: DataInputFormTester,
|
|
41
|
+
props: true,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
path: '/md',
|
|
45
|
+
component: MarkdownRenderer,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
path: '/',
|
|
49
|
+
alias: ['/home'],
|
|
50
|
+
name: 'home',
|
|
51
|
+
component: Home,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
path: '/about',
|
|
55
|
+
name: 'about',
|
|
56
|
+
component: About,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
path: '/login',
|
|
60
|
+
name: 'login',
|
|
61
|
+
component: Login,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
path: '/signup',
|
|
65
|
+
name: 'signup',
|
|
66
|
+
component: SignUp,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
path: '/notes',
|
|
70
|
+
component: ReleaseNotes,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
path: '/edit/:course',
|
|
74
|
+
props: true,
|
|
75
|
+
component: CourseEditor,
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
path: '/study',
|
|
79
|
+
name: 'study',
|
|
80
|
+
component: Study,
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
path: '/study/:focusCourseID',
|
|
84
|
+
component: Study,
|
|
85
|
+
props: true,
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
path: '/random',
|
|
89
|
+
name: 'random',
|
|
90
|
+
alias: ['/r'],
|
|
91
|
+
props: {
|
|
92
|
+
randomPreview: true,
|
|
93
|
+
},
|
|
94
|
+
component: Study,
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
path: '/classrooms',
|
|
98
|
+
name: 'classrooms',
|
|
99
|
+
component: Classrooms,
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
path: '/classrooms/:classroomId',
|
|
103
|
+
props: true,
|
|
104
|
+
alias: '/c/:classroomId',
|
|
105
|
+
component: ClassroomCtrlPanel,
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
path: '/classrooms/:classroomId/code',
|
|
109
|
+
props: true,
|
|
110
|
+
alias: '/c/:classroomId',
|
|
111
|
+
component: JoinCode,
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
path: '/courses',
|
|
115
|
+
alias: ['/quilts', '/q'],
|
|
116
|
+
component: Courses,
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
path: '/courses/:query',
|
|
120
|
+
props: true,
|
|
121
|
+
alias: ['/quilts/:query', '/q/:query'],
|
|
122
|
+
component: CourseRouter,
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
path: '/courses/:courseId/elo',
|
|
126
|
+
props: true,
|
|
127
|
+
alias: ['/quilts/:courseId/elo', '/q/:courseId/elo'],
|
|
128
|
+
component: ELOModerator,
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
path: '/courses/:courseId/tags/:tagId',
|
|
132
|
+
props: true,
|
|
133
|
+
alias: ['/quilts/:courseId/tags/:tagId', '/q/:courseId/tags/:tagId'],
|
|
134
|
+
component: TagInformation,
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
path: '/courses/:previewCourseID/preview',
|
|
138
|
+
props: true,
|
|
139
|
+
alias: ['/quilts/:previewCourseID/preview', '/q/:previewCourseID/preview'],
|
|
140
|
+
component: Study,
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
path: '/admin',
|
|
144
|
+
component: Admin,
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
path: '/user/:username',
|
|
148
|
+
alias: '/u/:username',
|
|
149
|
+
props: true,
|
|
150
|
+
component: User,
|
|
151
|
+
children: [
|
|
152
|
+
{
|
|
153
|
+
path: 'new',
|
|
154
|
+
component: User,
|
|
155
|
+
}, //,
|
|
156
|
+
// {
|
|
157
|
+
// path: '/stats',
|
|
158
|
+
// component: Stats,
|
|
159
|
+
// },
|
|
160
|
+
],
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
path: '/user/:_id/stats',
|
|
164
|
+
props: true,
|
|
165
|
+
alias: ['/u/:_id/stats'],
|
|
166
|
+
component: Stats,
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
router.beforeEach((to, from, next) => {
|
|
172
|
+
// paths that should be handled by the server, not the SPA
|
|
173
|
+
const apiPaths = ['/express', '/couch'];
|
|
174
|
+
|
|
175
|
+
if (apiPaths.some((path) => to.path.startsWith(path))) {
|
|
176
|
+
// Return false to cancel navigation and let the browser handle the request
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Continue with navigation for all other routes
|
|
181
|
+
next();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
export default router;
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { vi, describe, test, expect, beforeEach } from 'vitest';
|
|
2
|
+
import serverRequest from './index';
|
|
3
|
+
import { ServerRequestType, Status, CreateClassroom } from '@vue-skuilder/common';
|
|
4
|
+
|
|
5
|
+
// Instead of extending ServerRequest, directly use the CreateClassroom type
|
|
6
|
+
describe('serverRequest', () => {
|
|
7
|
+
let mockXHR: {
|
|
8
|
+
open: ReturnType<typeof vi.fn>;
|
|
9
|
+
send: ReturnType<typeof vi.fn>;
|
|
10
|
+
setRequestHeader: ReturnType<typeof vi.fn>;
|
|
11
|
+
withCredentials: boolean;
|
|
12
|
+
timeout: number;
|
|
13
|
+
onload?: () => void;
|
|
14
|
+
ontimeout?: () => void;
|
|
15
|
+
onerror?: () => void;
|
|
16
|
+
readyState?: number;
|
|
17
|
+
status?: number;
|
|
18
|
+
responseText?: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// Create a base test request that matches CreateClassroom exactly
|
|
22
|
+
const baseTestRequest: CreateClassroom = {
|
|
23
|
+
type: ServerRequestType.CREATE_CLASSROOM,
|
|
24
|
+
user: 'testuser',
|
|
25
|
+
data: {
|
|
26
|
+
students: [],
|
|
27
|
+
teachers: ['testuser'],
|
|
28
|
+
name: 'Test Classroom',
|
|
29
|
+
classMeetingSchedule: 'Never',
|
|
30
|
+
peerAssist: false,
|
|
31
|
+
joinCode: 'TEST123',
|
|
32
|
+
},
|
|
33
|
+
response: null,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
mockXHR = {
|
|
38
|
+
open: vi.fn(),
|
|
39
|
+
send: vi.fn(),
|
|
40
|
+
setRequestHeader: vi.fn(),
|
|
41
|
+
withCredentials: false,
|
|
42
|
+
timeout: 0,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// eslint-disable-next-line
|
|
46
|
+
global.XMLHttpRequest = vi.fn(() => mockXHR) as any;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('configures XMLHttpRequest correctly', async () => {
|
|
50
|
+
// Create a valid request with a timeout
|
|
51
|
+
const testRequest: CreateClassroom = {
|
|
52
|
+
...baseTestRequest,
|
|
53
|
+
timeout: 7000,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Start the request
|
|
57
|
+
const requestPromise = serverRequest(testRequest);
|
|
58
|
+
|
|
59
|
+
// Check that XMLHttpRequest was configured correctly
|
|
60
|
+
expect(mockXHR.open).toHaveBeenCalledWith('POST', expect.any(String), true);
|
|
61
|
+
expect(mockXHR.withCredentials).toBe(true);
|
|
62
|
+
expect(mockXHR.setRequestHeader).toHaveBeenCalledWith('Content-Type', 'application/json');
|
|
63
|
+
expect(mockXHR.timeout).toBe(7000);
|
|
64
|
+
expect(mockXHR.send).toHaveBeenCalledWith(JSON.stringify(testRequest));
|
|
65
|
+
|
|
66
|
+
// Simulate successful response with the expected format for CreateClassroom
|
|
67
|
+
mockXHR.responseText = JSON.stringify({
|
|
68
|
+
status: Status.ok,
|
|
69
|
+
ok: true,
|
|
70
|
+
joincode: 'ABC123',
|
|
71
|
+
uuid: '123-456-789',
|
|
72
|
+
});
|
|
73
|
+
if (mockXHR.onload) mockXHR.onload();
|
|
74
|
+
|
|
75
|
+
// Wait for the request to complete
|
|
76
|
+
const result = await requestPromise;
|
|
77
|
+
|
|
78
|
+
// Verify the result matches the expected format
|
|
79
|
+
expect(result.response).toEqual({
|
|
80
|
+
status: Status.ok,
|
|
81
|
+
ok: true,
|
|
82
|
+
joincode: 'ABC123',
|
|
83
|
+
uuid: '123-456-789',
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('handles successful response', async () => {
|
|
88
|
+
// Create a promise that will complete when onload is triggered
|
|
89
|
+
const requestPromise = serverRequest(baseTestRequest);
|
|
90
|
+
|
|
91
|
+
// Simulate a successful response that matches the expected structure
|
|
92
|
+
mockXHR.responseText = JSON.stringify({
|
|
93
|
+
status: Status.ok,
|
|
94
|
+
ok: true,
|
|
95
|
+
joincode: 'TEST123',
|
|
96
|
+
uuid: '123-456',
|
|
97
|
+
});
|
|
98
|
+
if (mockXHR.onload) mockXHR.onload();
|
|
99
|
+
|
|
100
|
+
// Wait for the request to complete
|
|
101
|
+
const result = await requestPromise;
|
|
102
|
+
|
|
103
|
+
// Verify the response was processed correctly
|
|
104
|
+
expect(result.response).toEqual({
|
|
105
|
+
status: Status.ok,
|
|
106
|
+
ok: true,
|
|
107
|
+
joincode: 'TEST123',
|
|
108
|
+
uuid: '123-456',
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('handles JSON parse error in response', async () => {
|
|
113
|
+
// Create a promise that will complete when onload is triggered
|
|
114
|
+
const requestPromise = serverRequest(baseTestRequest);
|
|
115
|
+
|
|
116
|
+
// Simulate an invalid JSON response
|
|
117
|
+
mockXHR.responseText = 'Not valid JSON';
|
|
118
|
+
if (mockXHR.onload) mockXHR.onload();
|
|
119
|
+
|
|
120
|
+
// Wait for the request to complete
|
|
121
|
+
const result = await requestPromise;
|
|
122
|
+
|
|
123
|
+
// Verify error handling
|
|
124
|
+
expect(result.response).toEqual({
|
|
125
|
+
status: Status.error,
|
|
126
|
+
ok: false,
|
|
127
|
+
errorText: expect.stringContaining('Failed to parse response'),
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('handles request timeout', async () => {
|
|
132
|
+
// Create a request with custom timeout
|
|
133
|
+
const testRequest: CreateClassroom = {
|
|
134
|
+
...baseTestRequest,
|
|
135
|
+
timeout: 5000,
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// Create a promise
|
|
139
|
+
const requestPromise = serverRequest(testRequest);
|
|
140
|
+
|
|
141
|
+
// Verify timeout is set correctly
|
|
142
|
+
expect(mockXHR.timeout).toBe(5000);
|
|
143
|
+
|
|
144
|
+
// Simulate timeout
|
|
145
|
+
if (mockXHR.ontimeout) mockXHR.ontimeout();
|
|
146
|
+
|
|
147
|
+
// Wait for the request to complete
|
|
148
|
+
const result = await requestPromise;
|
|
149
|
+
|
|
150
|
+
// Verify timeout handling
|
|
151
|
+
expect(result.response).toEqual({
|
|
152
|
+
status: Status.error,
|
|
153
|
+
ok: false,
|
|
154
|
+
errorText: 'Request timed out',
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('handles network error', async () => {
|
|
159
|
+
// Create a request
|
|
160
|
+
const requestPromise = serverRequest(baseTestRequest);
|
|
161
|
+
|
|
162
|
+
// Simulate network error
|
|
163
|
+
if (mockXHR.onerror) mockXHR.onerror();
|
|
164
|
+
|
|
165
|
+
// Wait for the request to complete
|
|
166
|
+
const result = await requestPromise;
|
|
167
|
+
|
|
168
|
+
// Verify error handling
|
|
169
|
+
expect(result.response).toEqual({
|
|
170
|
+
status: Status.error,
|
|
171
|
+
ok: false,
|
|
172
|
+
errorText: 'Network error occurred',
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('handles exception during request setup', async () => {
|
|
177
|
+
// Make XMLHttpRequest.open throw an error
|
|
178
|
+
mockXHR.open = vi.fn().mockImplementation(() => {
|
|
179
|
+
throw new Error('Connection refused');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Make the request which should catch the error
|
|
183
|
+
const result = await serverRequest(baseTestRequest);
|
|
184
|
+
|
|
185
|
+
// Verify error is caught and handled
|
|
186
|
+
expect(result.response).toEqual({
|
|
187
|
+
status: Status.error,
|
|
188
|
+
ok: false,
|
|
189
|
+
errorText: 'Connection refused',
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Status } from '@vue-skuilder/common';
|
|
2
|
+
import ENV from '@/ENVIRONMENT_VARS';
|
|
3
|
+
import { ServerRequest } from '@vue-skuilder/common';
|
|
4
|
+
|
|
5
|
+
const SERVER = ENV.EXPRESS_SERVER_PROTOCOL + '://' + ENV.EXPRESS_SERVER_URL;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Makes an authenticated request to the express backend and returns the response.
|
|
9
|
+
|
|
10
|
+
* @param requestData
|
|
11
|
+
* @returns
|
|
12
|
+
*/
|
|
13
|
+
export default async function serverRequest<T extends ServerRequest>(requestData: T): Promise<T> {
|
|
14
|
+
return new Promise<T>((resolve) => {
|
|
15
|
+
try {
|
|
16
|
+
const xml = new XMLHttpRequest();
|
|
17
|
+
xml.withCredentials = true;
|
|
18
|
+
xml.open('POST', SERVER, true);
|
|
19
|
+
xml.setRequestHeader('Content-Type', 'application/json');
|
|
20
|
+
xml.timeout = requestData.timeout || 7000;
|
|
21
|
+
|
|
22
|
+
// Handle the response when it completes
|
|
23
|
+
xml.onload = function () {
|
|
24
|
+
try {
|
|
25
|
+
requestData.response = JSON.parse(xml.responseText);
|
|
26
|
+
resolve(requestData);
|
|
27
|
+
} catch (parseError) {
|
|
28
|
+
requestData.response = {
|
|
29
|
+
status: Status.error,
|
|
30
|
+
ok: false,
|
|
31
|
+
errorText: `Failed to parse response: ${
|
|
32
|
+
parseError instanceof Error ? parseError.message : JSON.stringify(parseError)
|
|
33
|
+
}`,
|
|
34
|
+
};
|
|
35
|
+
resolve(requestData);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Handle network errors
|
|
40
|
+
xml.onerror = function () {
|
|
41
|
+
requestData.response = {
|
|
42
|
+
status: Status.error,
|
|
43
|
+
ok: false,
|
|
44
|
+
errorText: 'Network error occurred',
|
|
45
|
+
};
|
|
46
|
+
resolve(requestData);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Handle timeouts
|
|
50
|
+
xml.ontimeout = function () {
|
|
51
|
+
requestData.response = {
|
|
52
|
+
status: Status.error,
|
|
53
|
+
ok: false,
|
|
54
|
+
errorText: 'Request timed out',
|
|
55
|
+
};
|
|
56
|
+
resolve(requestData);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Send the request
|
|
60
|
+
xml.send(JSON.stringify(requestData));
|
|
61
|
+
} catch (error) {
|
|
62
|
+
// Handle any errors that occur during setup
|
|
63
|
+
requestData.response = {
|
|
64
|
+
status: Status.error,
|
|
65
|
+
ok: false,
|
|
66
|
+
errorText: error instanceof Error ? error.message : JSON.stringify(error),
|
|
67
|
+
};
|
|
68
|
+
resolve(requestData);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// // src/mock/store.ts
|
|
2
|
+
|
|
3
|
+
// import Vue from 'vue';
|
|
4
|
+
// import { DataShape } from '@/base-course/Interfaces/DataShape';
|
|
5
|
+
// import { ViewData } from '@/base-course/Interfaces/ViewData';
|
|
6
|
+
// import { CourseConfig } from '@/server/types';
|
|
7
|
+
// import { FieldInputInstance } from '@/components/Edit/ViewableDataInputForm/FieldInput.types';
|
|
8
|
+
// import { User } from '@vue-skuilder/db';
|
|
9
|
+
|
|
10
|
+
// export const GuestUsername: string = 'Guest';
|
|
11
|
+
|
|
12
|
+
// interface DataInputForm {
|
|
13
|
+
// dataShape: DataShape | null;
|
|
14
|
+
// course: CourseConfig | null;
|
|
15
|
+
// existingData: ViewData[];
|
|
16
|
+
// shapeViews: any[];
|
|
17
|
+
// fields: FieldInputInstance[];
|
|
18
|
+
// localStore: any;
|
|
19
|
+
// uploading: boolean;
|
|
20
|
+
// }
|
|
21
|
+
|
|
22
|
+
// export interface UserConfig {
|
|
23
|
+
// darkMode: boolean;
|
|
24
|
+
// likesConfetti: boolean;
|
|
25
|
+
// }
|
|
26
|
+
|
|
27
|
+
// export interface AppState {
|
|
28
|
+
// _user?: User;
|
|
29
|
+
// userLoginAndRegistrationContainer: {
|
|
30
|
+
// init: boolean;
|
|
31
|
+
// loggedIn: boolean;
|
|
32
|
+
// regDialogOpen: boolean;
|
|
33
|
+
// loginDialogOpen: boolean;
|
|
34
|
+
// };
|
|
35
|
+
// cardPreviewMode: boolean;
|
|
36
|
+
// dataInputForm: DataInputForm;
|
|
37
|
+
// views: {
|
|
38
|
+
// study: {
|
|
39
|
+
// inSession: boolean;
|
|
40
|
+
// courseList: string[];
|
|
41
|
+
// sessionTimeLimit: number;
|
|
42
|
+
// };
|
|
43
|
+
// };
|
|
44
|
+
// config: UserConfig;
|
|
45
|
+
// onLoadComplete: boolean;
|
|
46
|
+
// }
|
|
47
|
+
|
|
48
|
+
// export const defaultState: AppState = {
|
|
49
|
+
// _user: undefined,
|
|
50
|
+
// userLoginAndRegistrationContainer: {
|
|
51
|
+
// init: false,
|
|
52
|
+
// loggedIn: false,
|
|
53
|
+
// regDialogOpen: false,
|
|
54
|
+
// loginDialogOpen: false,
|
|
55
|
+
// },
|
|
56
|
+
// cardPreviewMode: false,
|
|
57
|
+
// dataInputForm: {
|
|
58
|
+
// course: null,
|
|
59
|
+
// dataShape: null,
|
|
60
|
+
// existingData: [],
|
|
61
|
+
// fields: [],
|
|
62
|
+
// localStore: {},
|
|
63
|
+
// shapeViews: [],
|
|
64
|
+
// uploading: false,
|
|
65
|
+
// },
|
|
66
|
+
// views: {
|
|
67
|
+
// study: {
|
|
68
|
+
// inSession: false,
|
|
69
|
+
// courseList: [],
|
|
70
|
+
// sessionTimeLimit: 5,
|
|
71
|
+
// },
|
|
72
|
+
// },
|
|
73
|
+
// config: {
|
|
74
|
+
// darkMode: false,
|
|
75
|
+
// likesConfetti: false,
|
|
76
|
+
// },
|
|
77
|
+
// onLoadComplete: false,
|
|
78
|
+
// };
|
|
79
|
+
|
|
80
|
+
// const Store = new Vuex.Store<AppState>({
|
|
81
|
+
// state: defaultState,
|
|
82
|
+
// mutations: {
|
|
83
|
+
// setUser(state, user: User) {
|
|
84
|
+
// state._user = user;
|
|
85
|
+
// },
|
|
86
|
+
// setLoggedIn(state, loggedIn: boolean) {
|
|
87
|
+
// state.userLoginAndRegistrationContainer.loggedIn = loggedIn;
|
|
88
|
+
// },
|
|
89
|
+
// setConfig(state, config: UserConfig) {
|
|
90
|
+
// state.config = config;
|
|
91
|
+
// },
|
|
92
|
+
// setOnLoadComplete(state, complete: boolean) {
|
|
93
|
+
// state.onLoadComplete = complete;
|
|
94
|
+
// },
|
|
95
|
+
// },
|
|
96
|
+
// actions: {
|
|
97
|
+
// async checkAuth({ commit }) {
|
|
98
|
+
// // Simulate auth check
|
|
99
|
+
// const isLoggedIn = Math.random() > 0.5; // 50% chance of being logged in
|
|
100
|
+
// if (isLoggedIn) {
|
|
101
|
+
// const mockUser = { username: 'MockUser' } as User;
|
|
102
|
+
// commit('setUser', mockUser);
|
|
103
|
+
// commit('setLoggedIn', true);
|
|
104
|
+
// commit('setConfig', { darkMode: false, likesConfetti: true });
|
|
105
|
+
// } else {
|
|
106
|
+
// const guestUser = { username: GuestUsername } as User;
|
|
107
|
+
// commit('setUser', guestUser);
|
|
108
|
+
// commit('setLoggedIn', false);
|
|
109
|
+
// }
|
|
110
|
+
// commit('setOnLoadComplete', true);
|
|
111
|
+
// },
|
|
112
|
+
// },
|
|
113
|
+
// });
|
|
114
|
+
|
|
115
|
+
// export default Store;
|
|
116
|
+
|
|
117
|
+
// export function setDefaultState() {
|
|
118
|
+
// Store.state.config = defaultState.config;
|
|
119
|
+
// }
|
|
120
|
+
|
|
121
|
+
// // Simulate checking auth on load
|
|
122
|
+
// Store.dispatch('checkAuth');
|