Set up cypress component testing
This commit is contained in:
parent
5576c21cb9
commit
8368050683
|
|
@ -60,7 +60,7 @@ module.exports = {
|
||||||
},
|
},
|
||||||
compilerOptions: {
|
compilerOptions: {
|
||||||
compatConfig: {
|
compatConfig: {
|
||||||
MODE: 2,
|
MODE: 3,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { resolve } from 'path';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
chromeWebSecurity: false,
|
chromeWebSecurity: false,
|
||||||
|
|
||||||
e2e: {
|
e2e: {
|
||||||
baseUrl: 'http://localhost:8080',
|
baseUrl: 'http://localhost:8080',
|
||||||
specPattern: 'cypress/e2e/frontend/**/*.{cy,spec}.{js,ts}',
|
specPattern: 'cypress/e2e/frontend/**/*.{cy,spec}.{js,ts}',
|
||||||
|
|
@ -16,14 +17,29 @@ export default defineConfig({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
videoUploadOnPasses: false,
|
videoUploadOnPasses: false,
|
||||||
reporter: 'junit',
|
reporter: 'junit',
|
||||||
|
|
||||||
reporterOptions: {
|
reporterOptions: {
|
||||||
mochaFile: 'cypress/test-reports/frontend/cypress-results-[hash].xml',
|
mochaFile: 'cypress/test-reports/frontend/cypress-results-[hash].xml',
|
||||||
toConsole: true,
|
toConsole: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
projectId: 'msk-fe',
|
projectId: 'msk-fe',
|
||||||
|
|
||||||
retries: {
|
retries: {
|
||||||
runMode: 3,
|
runMode: 3,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
component: {
|
||||||
|
devServer: {
|
||||||
|
framework: 'vue',
|
||||||
|
bundler: 'webpack',
|
||||||
|
webpackConfig: async () => {
|
||||||
|
const webpackConfig = await require('./build/webpack.dev.conf');
|
||||||
|
return webpackConfig;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import PageFormInput from '@/components/page-form/PageFormInput.vue';
|
||||||
|
|
||||||
|
describe('<PageFormInput />', () => {
|
||||||
|
it('renders', () => {
|
||||||
|
// see: https://test-utils.vuejs.org/guide/
|
||||||
|
const inputSpy = cy.spy().as('inputSpy');
|
||||||
|
cy.mount(PageFormInput, {
|
||||||
|
props: {
|
||||||
|
label: 'Hi',
|
||||||
|
value: 'Some',
|
||||||
|
onInput: inputSpy,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
cy.get('.page-form-input__label').should('contain.text', 'Hi');
|
||||||
|
cy.getByDataCy('page-form-input-hi').should('have.value', 'Some').type('A');
|
||||||
|
cy.get('@inputSpy').should('have.been.calledWith', 'SomeA');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import SubmissionForm from '@/components/content-blocks/assignment/SubmissionForm.vue';
|
||||||
|
|
||||||
|
describe('SubmissionForm', () => {
|
||||||
|
it('renders', () => {
|
||||||
|
cy.mount(SubmissionForm, {
|
||||||
|
props: {
|
||||||
|
userInput: {
|
||||||
|
final: false,
|
||||||
|
text: 'userInput',
|
||||||
|
},
|
||||||
|
saved: true,
|
||||||
|
placeholder: 'Placeholder',
|
||||||
|
action: 'Feedback teilen',
|
||||||
|
reopen: () => { },
|
||||||
|
document: '',
|
||||||
|
sharedMsg: 'Shared Message',
|
||||||
|
onTurnIn: () => { },
|
||||||
|
onSaveInput: () => { },
|
||||||
|
onReopen: () => { },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import SubmissionInput from '@/components/content-blocks/assignment/SubmissionInput.vue';
|
||||||
|
|
||||||
|
describe('SubmissionInput', () => {
|
||||||
|
it('renders', () => {
|
||||||
|
cy.mount(SubmissionInput, {
|
||||||
|
props: {
|
||||||
|
inputText: undefined,
|
||||||
|
saved: true,
|
||||||
|
readonly: false,
|
||||||
|
placeholder: 'Placeholder',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
cy.getByDataCy('submission-textarea')
|
||||||
|
.should('have.attr', 'placeholder', 'Placeholder')
|
||||||
|
.type('Hallo Velo')
|
||||||
|
.should('have.value', 'Hallo Velo');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -27,124 +27,17 @@
|
||||||
//
|
//
|
||||||
import { makeExecutableSchema } from '@graphql-tools/schema';
|
import { makeExecutableSchema } from '@graphql-tools/schema';
|
||||||
|
|
||||||
declare global {
|
|
||||||
namespace Cypress {
|
|
||||||
interface Chainable {
|
|
||||||
/**
|
|
||||||
* Login via API call to the GraphQL endpoint, without calling the frontend. Faster than the other login.
|
|
||||||
* @param username
|
|
||||||
* @param password
|
|
||||||
* @example
|
|
||||||
* cy.apolloLogin('ross.geller', 'test')
|
|
||||||
*/
|
|
||||||
apolloLogin(username: string, password: string): Chainable<any>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Selects an element based on the `data-cy=xxx` attribute
|
|
||||||
* @param selector - The value of the data-cy attribute to select
|
|
||||||
* @example
|
|
||||||
* cy.getByDataCy('my-new-button')
|
|
||||||
*/
|
|
||||||
getByDataCy(selector: string): Chainable<Element>;
|
|
||||||
|
|
||||||
selectClass(schoolClass: string): void;
|
|
||||||
|
|
||||||
login(username: string, password: string, visitLogin?: boolean): void;
|
|
||||||
|
|
||||||
fakeLogin(username: string, password: string): void;
|
|
||||||
|
|
||||||
isSubmissionReadOnly(myText: string): void;
|
|
||||||
|
|
||||||
openSidebar(): void;
|
|
||||||
|
|
||||||
setup(): void;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// installed a fork of the original package, because of this issue:
|
|
||||||
// https://github.com/tgriesser/cypress-graphql-mock/issues/23
|
|
||||||
// todo: once above issue is fixed, go back to the original repo -> npm install cypress-graphql-mock
|
|
||||||
// import 'cypress-graphql-mock';
|
|
||||||
import mocks from '../fixtures/mocks';
|
|
||||||
import { addMocksToSchema } from '@graphql-tools/mock';
|
|
||||||
import { graphql, GraphQLError } from 'graphql';
|
|
||||||
|
|
||||||
Cypress.Commands.add('apolloLogin', (username, password) => {
|
|
||||||
const payload = {
|
|
||||||
operationName: 'BetaLogin',
|
|
||||||
variables: {
|
|
||||||
input: {
|
|
||||||
usernameInput: username,
|
|
||||||
passwordInput: password,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
query:
|
|
||||||
'mutation BetaLogin($input: BetaLoginInput!) {\n betaLogin(input: $input) {\n success\n __typename\n }\n}\n',
|
|
||||||
};
|
|
||||||
|
|
||||||
cy.request({
|
|
||||||
method: 'POST',
|
|
||||||
url: '/api/graphql-public/',
|
|
||||||
body: payload,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// todo: replace with apollo call
|
|
||||||
Cypress.Commands.add('login', (username, password, visitLogin = false) => {
|
|
||||||
if (visitLogin) {
|
|
||||||
cy.visit('/beta-login');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (username !== '') {
|
|
||||||
cy.get('[data-cy=email-input]').type(username);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (password !== '') {
|
|
||||||
cy.get('[data-cy=password-input]').type(password);
|
|
||||||
}
|
|
||||||
cy.get('[data-cy=login-button]').click();
|
|
||||||
});
|
|
||||||
|
|
||||||
Cypress.Commands.add('getByDataCy', (selector: string) => {
|
|
||||||
return cy.get(`[data-cy=${selector}]`);
|
|
||||||
});
|
|
||||||
|
|
||||||
Cypress.Commands.add('selectClass', (schoolClass) => {
|
|
||||||
cy.getByDataCy('user-widget-avatar').click();
|
|
||||||
cy.getByDataCy('class-selection').click();
|
|
||||||
cy.getByDataCy('class-selection-entry').contains(schoolClass).click();
|
|
||||||
});
|
|
||||||
|
|
||||||
Cypress.Commands.add('fakeLogin', () => {
|
|
||||||
cy.log('Logging in (fake)');
|
|
||||||
cy.setCookie('loginStatus', 'true');
|
|
||||||
});
|
|
||||||
|
|
||||||
Cypress.Commands.add('isSubmissionReadOnly', (myText) => {
|
|
||||||
cy.get('.submission-form__textarea--readonly').as('textarea');
|
|
||||||
|
|
||||||
cy.get('@textarea').invoke('val').should('contain', myText);
|
|
||||||
cy.get('@textarea').should('have.attr', 'readonly');
|
|
||||||
cy.getByDataCy('submission-form-submit').should('not.exist');
|
|
||||||
});
|
|
||||||
|
|
||||||
Cypress.Commands.add('openSidebar', () => {
|
|
||||||
cy.getByDataCy('user-widget-avatar').click();
|
|
||||||
});
|
|
||||||
|
|
||||||
Cypress.Commands.add('setup', () => {
|
|
||||||
cy.fakeLogin('nino.teacher', 'test');
|
|
||||||
cy.viewport('macbook-15');
|
|
||||||
cy.mockGraphql();
|
|
||||||
});
|
|
||||||
|
|
||||||
const typenameResolver = {
|
const typenameResolver = {
|
||||||
__resolveType(obj, context, info) {
|
__resolveType(obj, context, info) {
|
||||||
return obj.__typename;
|
return obj.__typename;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
Cypress.Commands.add('mockGraphql', (options?: any) => {
|
const getByDataCy = (selector: string) => {
|
||||||
|
return cy.get(`[data-cy=${selector}]`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockGraphql = (options?: any) => {
|
||||||
cy.task('getSchema').then((schemaString: string) => {
|
cy.task('getSchema').then((schemaString: string) => {
|
||||||
const resolverMap = {
|
const resolverMap = {
|
||||||
DeleteSnapshotResult: typenameResolver,
|
DeleteSnapshotResult: typenameResolver,
|
||||||
|
|
@ -214,12 +107,129 @@ Cypress.Commands.add('mockGraphql', (options?: any) => {
|
||||||
},
|
},
|
||||||
}).as('mockGraphqlOps');
|
}).as('mockGraphqlOps');
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockGraphqlOps = (options) => {
|
||||||
|
cy.get('@mockGraphqlOps').invoke('setOperations' as any, options);
|
||||||
|
};
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace Cypress {
|
||||||
|
interface Chainable {
|
||||||
|
/**
|
||||||
|
* Login via API call to the GraphQL endpoint, without calling the frontend. Faster than the other login.
|
||||||
|
* @param username
|
||||||
|
* @param password
|
||||||
|
* @example
|
||||||
|
* cy.apolloLogin('ross.geller', 'test')
|
||||||
|
*/
|
||||||
|
apolloLogin(username: string, password: string): Chainable<any>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selects an element based on the `data-cy=xxx` attribute
|
||||||
|
* @param selector - The value of the data-cy attribute to select
|
||||||
|
* @example
|
||||||
|
* cy.getByDataCy('my-new-button')
|
||||||
|
*/
|
||||||
|
// getByDataCy(selector: string): Chainable<Element>;
|
||||||
|
getByDataCy: typeof getByDataCy;
|
||||||
|
|
||||||
|
selectClass(schoolClass: string): void;
|
||||||
|
|
||||||
|
login(username: string, password: string, visitLogin?: boolean): void;
|
||||||
|
|
||||||
|
fakeLogin(username: string, password: string): void;
|
||||||
|
|
||||||
|
isSubmissionReadOnly(myText: string): void;
|
||||||
|
|
||||||
|
openSidebar(): void;
|
||||||
|
|
||||||
|
setup(): void;
|
||||||
|
|
||||||
|
mockGraphql: typeof mockGraphql;
|
||||||
|
mockGraphqlOps: typeof mockGraphqlOps;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// installed a fork of the original package, because of this issue:
|
||||||
|
// https://github.com/tgriesser/cypress-graphql-mock/issues/23
|
||||||
|
// todo: once above issue is fixed, go back to the original repo -> npm install cypress-graphql-mock
|
||||||
|
// import 'cypress-graphql-mock';
|
||||||
|
import mocks from '../fixtures/mocks';
|
||||||
|
import { addMocksToSchema } from '@graphql-tools/mock';
|
||||||
|
import { graphql, GraphQLError } from 'graphql';
|
||||||
|
|
||||||
|
Cypress.Commands.add('apolloLogin', (username, password) => {
|
||||||
|
const payload = {
|
||||||
|
operationName: 'BetaLogin',
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
usernameInput: username,
|
||||||
|
passwordInput: password,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
query:
|
||||||
|
'mutation BetaLogin($input: BetaLoginInput!) {\n betaLogin(input: $input) {\n success\n __typename\n }\n}\n',
|
||||||
|
};
|
||||||
|
|
||||||
|
cy.request({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/graphql-public/',
|
||||||
|
body: payload,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
Cypress.Commands.add('mockGraphqlOps', (options) => {
|
// todo: replace with apollo call
|
||||||
cy.get('@mockGraphqlOps').invoke('setOperations' as any, options);
|
Cypress.Commands.add('login', (username, password, visitLogin = false) => {
|
||||||
|
if (visitLogin) {
|
||||||
|
cy.visit('/beta-login');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (username !== '') {
|
||||||
|
cy.get('[data-cy=email-input]').type(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password !== '') {
|
||||||
|
cy.get('[data-cy=password-input]').type(password);
|
||||||
|
}
|
||||||
|
cy.get('[data-cy=login-button]').click();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Cypress.Commands.add('getByDataCy', getByDataCy);
|
||||||
|
|
||||||
|
Cypress.Commands.add('selectClass', (schoolClass) => {
|
||||||
|
cy.getByDataCy('user-widget-avatar').click();
|
||||||
|
cy.getByDataCy('class-selection').click();
|
||||||
|
cy.getByDataCy('class-selection-entry').contains(schoolClass).click();
|
||||||
|
});
|
||||||
|
|
||||||
|
Cypress.Commands.add('fakeLogin', () => {
|
||||||
|
cy.log('Logging in (fake)');
|
||||||
|
cy.setCookie('loginStatus', 'true');
|
||||||
|
});
|
||||||
|
|
||||||
|
Cypress.Commands.add('isSubmissionReadOnly', (myText) => {
|
||||||
|
cy.get('.submission-form__textarea--readonly').as('textarea');
|
||||||
|
|
||||||
|
cy.get('@textarea').invoke('val').should('contain', myText);
|
||||||
|
cy.get('@textarea').should('have.attr', 'readonly');
|
||||||
|
cy.getByDataCy('submission-form-submit').should('not.exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
Cypress.Commands.add('openSidebar', () => {
|
||||||
|
cy.getByDataCy('user-widget-avatar').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
Cypress.Commands.add('setup', () => {
|
||||||
|
cy.fakeLogin('nino.teacher', 'test');
|
||||||
|
cy.viewport('macbook-15');
|
||||||
|
cy.mockGraphql();
|
||||||
|
});
|
||||||
|
|
||||||
|
Cypress.Commands.add('mockGraphql', mockGraphql);
|
||||||
|
|
||||||
|
Cypress.Commands.add('mockGraphqlOps', mockGraphqlOps);
|
||||||
|
|
||||||
const getRootValue = (allOperations: any, operationName: string, variables: any) => {
|
const getRootValue = (allOperations: any, operationName: string, variables: any) => {
|
||||||
const operation = allOperations[operationName];
|
const operation = allOperations[operationName];
|
||||||
if (typeof operation === 'function') {
|
if (typeof operation === 'function') {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<title>Components App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div data-cy-root></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
// ***********************************************************
|
||||||
|
// This example support/component.ts is processed and
|
||||||
|
// loaded automatically before your test files.
|
||||||
|
//
|
||||||
|
// This is a great place to put global configuration and
|
||||||
|
// behavior that modifies Cypress.
|
||||||
|
//
|
||||||
|
// You can change the location of this file or turn off
|
||||||
|
// automatically serving support files with the
|
||||||
|
// 'supportFile' configuration option.
|
||||||
|
//
|
||||||
|
// You can read more here:
|
||||||
|
// https://on.cypress.io/configuration
|
||||||
|
// ***********************************************************
|
||||||
|
|
||||||
|
// Import commands.js using ES2015 syntax:
|
||||||
|
import './commands';
|
||||||
|
|
||||||
|
// Alternatively you can use CommonJS syntax:
|
||||||
|
// require('./commands')
|
||||||
|
|
||||||
|
import { mount } from 'cypress/vue';
|
||||||
|
import '@/main.js';
|
||||||
|
|
||||||
|
// Augment the Cypress namespace to include type definitions for
|
||||||
|
// your custom command.
|
||||||
|
// Alternatively, can be defined in cypress/support/component.d.ts
|
||||||
|
// with a <reference path="./component" /> at the top of your spec.
|
||||||
|
declare global {
|
||||||
|
namespace Cypress {
|
||||||
|
interface Chainable {
|
||||||
|
mount: typeof mount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Cypress.Commands.add('mount', mount);
|
||||||
|
|
||||||
|
// Example use:
|
||||||
|
// cy.mount(MyComponent)
|
||||||
|
|
@ -87,7 +87,7 @@
|
||||||
"vee-validate": "^4.5.10",
|
"vee-validate": "^4.5.10",
|
||||||
"vue": "3.2.30",
|
"vue": "3.2.30",
|
||||||
"vue-loader": "^16.8.3",
|
"vue-loader": "^16.8.3",
|
||||||
"vue-matomo": "^4.1.0",
|
"vue-matomo": "^4.2.0",
|
||||||
"vue-router": "^4.0.14",
|
"vue-router": "^4.0.14",
|
||||||
"vue-scrollto": "^2.20.0",
|
"vue-scrollto": "^2.20.0",
|
||||||
"vue-style-loader": "^3.0.1",
|
"vue-style-loader": "^3.0.1",
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,7 @@
|
||||||
"vee-validate": "^4.5.10",
|
"vee-validate": "^4.5.10",
|
||||||
"vue": "3.2.30",
|
"vue": "3.2.30",
|
||||||
"vue-loader": "^16.8.3",
|
"vue-loader": "^16.8.3",
|
||||||
"vue-matomo": "^4.1.0",
|
"vue-matomo": "^4.2.0",
|
||||||
"vue-router": "^4.0.14",
|
"vue-router": "^4.0.14",
|
||||||
"vue-scrollto": "^2.20.0",
|
"vue-scrollto": "^2.20.0",
|
||||||
"vue-style-loader": "^3.0.1",
|
"vue-style-loader": "^3.0.1",
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,6 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '~styles/main.scss';
|
|
||||||
@import '~styles/helpers';
|
@import '~styles/helpers';
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
|
|
||||||
|
|
@ -1,162 +1,15 @@
|
||||||
import 'core-js/stable';
|
import 'core-js/stable';
|
||||||
import { createApp, configureCompat, h, provide } from 'vue';
|
import setupApp from './setup/setupApp';
|
||||||
import VueVimeoPlayer from 'vue-vimeo-player';
|
import registerPlugins from './setup/plugins';
|
||||||
import apolloClientFactory from './graphql/client';
|
import setupRouteGuards from './setup/router';
|
||||||
import App from './App.vue';
|
import registerDirectives from './setup/directives';
|
||||||
import { postLoginRedirectUrlKey, router } from './router';
|
import '@/styles/main.scss';
|
||||||
import { store } from '@/store';
|
|
||||||
import VueScrollTo from 'vue-scrollto';
|
|
||||||
import autoGrow from '@/directives/auto-grow';
|
|
||||||
import clickOutside from '@/directives/click-outside';
|
|
||||||
import ME_QUERY from '@/graphql/gql/queries/meQuery.gql';
|
|
||||||
import VueModal from '@/plugins/modal';
|
|
||||||
import VueRemoveEdges from '@/plugins/edges';
|
|
||||||
import VueMatomo from 'vue-matomo';
|
|
||||||
import { createApolloProvider } from '@vue/apollo-option';
|
|
||||||
import { joiningClass, loginRequired, unauthorizedAccess } from '@/router/guards';
|
|
||||||
import flavorPlugin from '@/plugins/flavor';
|
|
||||||
import { DefaultApolloClient } from '@vue/apollo-composable';
|
|
||||||
|
|
||||||
const publicApolloClient = apolloClientFactory('/api/graphql-public/', null);
|
const app = setupApp();
|
||||||
const privateApolloClient = apolloClientFactory('/api/graphql/', networkErrorCallback);
|
|
||||||
|
|
||||||
const apolloProvider = createApolloProvider({
|
registerPlugins(app);
|
||||||
clients: {
|
setupRouteGuards();
|
||||||
publicClient: publicApolloClient,
|
|
||||||
},
|
|
||||||
defaultClient: privateApolloClient,
|
|
||||||
});
|
|
||||||
|
|
||||||
configureCompat({
|
registerDirectives(app);
|
||||||
MODE: 3,
|
|
||||||
});
|
|
||||||
|
|
||||||
const app = createApp({
|
|
||||||
setup() {
|
|
||||||
provide(DefaultApolloClient, privateApolloClient);
|
|
||||||
},
|
|
||||||
render: () => h(App),
|
|
||||||
});
|
|
||||||
|
|
||||||
app.use(store);
|
|
||||||
|
|
||||||
app.use(VueModal);
|
|
||||||
app.use(VueRemoveEdges);
|
|
||||||
app.use(VueVimeoPlayer);
|
|
||||||
app.use(apolloProvider);
|
|
||||||
app.use(router);
|
|
||||||
|
|
||||||
// VueScrollTo.setDefaults({
|
|
||||||
// duration: 500,
|
|
||||||
// easing: 'ease-out',
|
|
||||||
// offset: -50,
|
|
||||||
// });
|
|
||||||
|
|
||||||
app.directive('scroll-to', VueScrollTo);
|
|
||||||
|
|
||||||
app.use(flavorPlugin);
|
|
||||||
|
|
||||||
if (process.env.MATOMO_HOST) {
|
|
||||||
app.use(VueMatomo, {
|
|
||||||
host: process.env.MATOMO_HOST,
|
|
||||||
siteId: process.env.MATOMO_SITE_ID,
|
|
||||||
router: router,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
app.directive('click-outside', clickOutside);
|
|
||||||
app.directive('auto-grow', autoGrow);
|
|
||||||
|
|
||||||
/* guards */
|
|
||||||
|
|
||||||
function redirectUsersWithoutValidLicense() {
|
|
||||||
return privateApolloClient
|
|
||||||
.query({
|
|
||||||
query: ME_QUERY,
|
|
||||||
})
|
|
||||||
.then(({ data }) => data.me.expiryDate == null);
|
|
||||||
}
|
|
||||||
|
|
||||||
function redirectStudentsWithoutClass() {
|
|
||||||
return privateApolloClient
|
|
||||||
.query({
|
|
||||||
query: ME_QUERY,
|
|
||||||
})
|
|
||||||
.then(({ data }) => data.me.schoolClasses.length === 0 && !data.me.isTeacher);
|
|
||||||
}
|
|
||||||
|
|
||||||
function redirectUsersToOnboarding() {
|
|
||||||
return privateApolloClient
|
|
||||||
.query({
|
|
||||||
query: ME_QUERY,
|
|
||||||
})
|
|
||||||
.then(({ data }) => !data.me.onboardingVisited);
|
|
||||||
}
|
|
||||||
|
|
||||||
function networkErrorCallback(statusCode) {
|
|
||||||
if (statusCode === 402) {
|
|
||||||
router.push({ name: 'licenseActivation' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
router.beforeEach(async (to, from, next) => {
|
|
||||||
// todo: make logger work outside vue app
|
|
||||||
// const logger = inject('vuejs3-logger');
|
|
||||||
// logger.$log.debug('navigation guard called', to, from);
|
|
||||||
if (to.path === '/logout') {
|
|
||||||
await publicApolloClient.resetStore();
|
|
||||||
if (process.env.LOGOUT_REDIRECT_URL) {
|
|
||||||
location.replace(`https://sso.hep-verlag.ch/logout?return_to=${process.env.LOGOUT_REDIRECT_URL}`);
|
|
||||||
next(false);
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
next({ name: 'hello' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (unauthorizedAccess(to)) {
|
|
||||||
//logger.$log.debug('unauthorized', to);
|
|
||||||
const postLoginRedirectionUrl = to.path;
|
|
||||||
const redirectUrl = `/hello/`;
|
|
||||||
|
|
||||||
if (window.localStorage) {
|
|
||||||
localStorage.setItem(postLoginRedirectUrlKey, postLoginRedirectionUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
// logger.$log.debug('redirecting to hello', to);
|
|
||||||
next(redirectUrl);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (to.name && to.name !== 'licenseActivation' && loginRequired(to) && (await redirectUsersWithoutValidLicense())) {
|
|
||||||
// logger.$log.debug('redirecting to licenseActivation', to, null);
|
|
||||||
console.log('redirecting to licenseActivation', to, null);
|
|
||||||
next({ name: 'licenseActivation' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!joiningClass(to) && loginRequired(to) && (await redirectStudentsWithoutClass())) {
|
|
||||||
//logger.$log.debug('redirecting to join-class', to);
|
|
||||||
//logger.$log.debug('await redirectStudentsWithoutClass()', await redirectStudentsWithoutClass());
|
|
||||||
next({ name: 'join-class' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
to.name &&
|
|
||||||
to.name.indexOf('onboarding') === -1 &&
|
|
||||||
!joiningClass(to) &&
|
|
||||||
loginRequired(to) &&
|
|
||||||
(await redirectUsersToOnboarding())
|
|
||||||
) {
|
|
||||||
//logger.$log.debug('redirecting to onboarding-start', to);
|
|
||||||
next({ name: 'onboarding-start' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//logger.$log.debug('End of Guard reached', to);
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
app.mount('#app');
|
app.mount('#app');
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import apolloClientFactory from '@/graphql/client';
|
||||||
|
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
function networkErrorCallback(statusCode: number) {
|
||||||
|
if (statusCode === 402) {
|
||||||
|
router.push({ name: 'licenseActivation' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createApolloClients = () => {
|
||||||
|
return {
|
||||||
|
publicApolloClient: apolloClientFactory('/api/graphql-public/', null),
|
||||||
|
privateApolloClient: apolloClientFactory('/api/graphql/', networkErrorCallback),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const apolloClients = createApolloClients();
|
||||||
|
|
||||||
|
export default apolloClients;
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
import autoGrow from '@/directives/auto-grow';
|
||||||
|
import clickOutside from '@/directives/click-outside';
|
||||||
|
const registerDirectives = (app: any) => {
|
||||||
|
app.directive('click-outside', clickOutside);
|
||||||
|
app.directive('auto-grow', autoGrow);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default registerDirectives;
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { router } from '@/router';
|
||||||
|
import VueVimeoPlayer from 'vue-vimeo-player';
|
||||||
|
import { store } from '@/store';
|
||||||
|
import VueModal from '@/plugins/modal';
|
||||||
|
import VueRemoveEdges from '@/plugins/edges';
|
||||||
|
import { createApolloProvider } from '@vue/apollo-option';
|
||||||
|
import apolloClients from './apollo';
|
||||||
|
import flavorPlugin from '@/plugins/flavor';
|
||||||
|
import VueMatomo from 'vue-matomo';
|
||||||
|
|
||||||
|
const { publicApolloClient, privateApolloClient } = apolloClients;
|
||||||
|
|
||||||
|
const apolloProvider = createApolloProvider({
|
||||||
|
clients: {
|
||||||
|
publicClient: publicApolloClient,
|
||||||
|
},
|
||||||
|
defaultClient: privateApolloClient,
|
||||||
|
});
|
||||||
|
const registerPlugins = (app: any) => {
|
||||||
|
app.use(store);
|
||||||
|
app.use(VueModal);
|
||||||
|
app.use(VueRemoveEdges);
|
||||||
|
app.use(VueVimeoPlayer);
|
||||||
|
app.use(apolloProvider);
|
||||||
|
app.use(router);
|
||||||
|
app.use(flavorPlugin);
|
||||||
|
if (process.env.MATOMO_HOST) {
|
||||||
|
app.use(VueMatomo, {
|
||||||
|
host: process.env.MATOMO_HOST,
|
||||||
|
siteId: process.env.MATOMO_SITE_ID,
|
||||||
|
router: router,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default registerPlugins;
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
import apolloClients from './apollo';
|
||||||
|
const { publicApolloClient, privateApolloClient } = apolloClients;
|
||||||
|
import ME_QUERY from '@/graphql/gql/queries/meQuery.gql';
|
||||||
|
import { joiningClass, loginRequired, unauthorizedAccess } from '@/router/guards';
|
||||||
|
import { postLoginRedirectUrlKey, router } from '@/router';
|
||||||
|
|
||||||
|
async function redirectUsersWithoutValidLicense() {
|
||||||
|
const { data } = await privateApolloClient.query({
|
||||||
|
query: ME_QUERY,
|
||||||
|
});
|
||||||
|
return data.me.expiryDate == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function redirectStudentsWithoutClass() {
|
||||||
|
const { data } = await privateApolloClient.query({
|
||||||
|
query: ME_QUERY,
|
||||||
|
});
|
||||||
|
return data.me.schoolClasses.length === 0 && !data.me.isTeacher;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function redirectUsersToOnboarding() {
|
||||||
|
const { data } = await privateApolloClient.query({
|
||||||
|
query: ME_QUERY,
|
||||||
|
});
|
||||||
|
return !data.me.onboardingVisited;
|
||||||
|
}
|
||||||
|
|
||||||
|
const setupRouteGuards = () => {
|
||||||
|
router.beforeEach(async (to, _, next) => {
|
||||||
|
// todo: make logger work outside vue app
|
||||||
|
// const logger = inject('vuejs3-logger');
|
||||||
|
// logger.$log.debug('navigation guard called', to, from);
|
||||||
|
if (to.path === '/logout') {
|
||||||
|
await publicApolloClient.resetStore();
|
||||||
|
if (process.env.LOGOUT_REDIRECT_URL) {
|
||||||
|
location.replace(`https://sso.hep-verlag.ch/logout?return_to=${process.env.LOGOUT_REDIRECT_URL}`);
|
||||||
|
next(false);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
next({ name: 'hello' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unauthorizedAccess(to)) {
|
||||||
|
//logger.$log.debug('unauthorized', to);
|
||||||
|
const postLoginRedirectionUrl = to.path;
|
||||||
|
const redirectUrl = `/hello/`;
|
||||||
|
|
||||||
|
if (window.localStorage) {
|
||||||
|
localStorage.setItem(postLoginRedirectUrlKey, postLoginRedirectionUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
// logger.$log.debug('redirecting to hello', to);
|
||||||
|
next(redirectUrl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (to.name && to.name !== 'licenseActivation' && loginRequired(to) && (await redirectUsersWithoutValidLicense())) {
|
||||||
|
// logger.$log.debug('redirecting to licenseActivation', to, null);
|
||||||
|
console.log('redirecting to licenseActivation', to, null);
|
||||||
|
next({ name: 'licenseActivation' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!joiningClass(to) && loginRequired(to) && (await redirectStudentsWithoutClass())) {
|
||||||
|
//logger.$log.debug('redirecting to join-class', to);
|
||||||
|
//logger.$log.debug('await redirectStudentsWithoutClass()', await redirectStudentsWithoutClass());
|
||||||
|
next({ name: 'join-class' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
to.name &&
|
||||||
|
(to.name as string).indexOf('onboarding') === -1 &&
|
||||||
|
!joiningClass(to) &&
|
||||||
|
loginRequired(to) &&
|
||||||
|
(await redirectUsersToOnboarding())
|
||||||
|
) {
|
||||||
|
//logger.$log.debug('redirecting to onboarding-start', to);
|
||||||
|
next({ name: 'onboarding-start' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//logger.$log.debug('End of Guard reached', to);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default setupRouteGuards;
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { createApp, h, provide } from 'vue';
|
||||||
|
import App from '@/App.vue';
|
||||||
|
|
||||||
|
import apolloClients from './apollo';
|
||||||
|
import { DefaultApolloClient } from '@vue/apollo-composable';
|
||||||
|
const { privateApolloClient } = apolloClients;
|
||||||
|
|
||||||
|
const setupApp = () => {
|
||||||
|
const app = createApp({
|
||||||
|
setup() {
|
||||||
|
provide(DefaultApolloClient, privateApolloClient);
|
||||||
|
},
|
||||||
|
render: () => h(App),
|
||||||
|
});
|
||||||
|
return app;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default setupApp;
|
||||||
|
|
@ -10,3 +10,13 @@ declare module '*.vue' {
|
||||||
const component: DefineComponent<{}, {}, any>;
|
const component: DefineComponent<{}, {}, any>;
|
||||||
export default component;
|
export default component;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ugly hack to make types for those two packages
|
||||||
|
declare module 'vue-vimeo-player' {
|
||||||
|
const plugin: any;
|
||||||
|
export default plugin;
|
||||||
|
}
|
||||||
|
declare module 'vue-matomo' {
|
||||||
|
const plugin: any;
|
||||||
|
export default plugin;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue