(initialKey)
14 |
15 | watch(activeSpinner, (key) => {
16 | if (key in pkgSpinners) router.replace({ query: { spinner: key } })
17 | })
18 |
19 | return {
20 | provide: {
21 | store: {
22 | activeSpinner,
23 | },
24 | },
25 | }
26 | })
27 |
--------------------------------------------------------------------------------
/playground/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smastrom/vue-global-loader/67d7bdb7577b9282996646ef1e7bf432c6da8e10/playground/public/favicon.ico
--------------------------------------------------------------------------------
/playground/public/og-image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smastrom/vue-global-loader/67d7bdb7577b9282996646ef1e7bf432c6da8e10/playground/public/og-image.jpg
--------------------------------------------------------------------------------
/playground/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow:
--------------------------------------------------------------------------------
/playground/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./.nuxt/tsconfig.json"
3 | }
4 |
--------------------------------------------------------------------------------
/playground/utils/head.ts:
--------------------------------------------------------------------------------
1 | const siteName = 'Vue Global Loader'
2 | const description = 'Global loaders made easy for Vue and Nuxt.'
3 |
4 | export function getHead() {
5 | return {
6 | title: `${siteName} - ${description}`,
7 | link: [
8 | {
9 | rel: 'icon',
10 | href: '/favicon.ico',
11 | },
12 | ],
13 | htmlAttrs: {
14 | lang: 'en',
15 | },
16 | meta: [
17 | {
18 | hid: 'description',
19 | name: 'description',
20 | content: description,
21 | },
22 | {
23 | hid: 'og:title',
24 | property: 'og:title',
25 | content: `${siteName} - ${description}`,
26 | },
27 | {
28 | hid: 'og:description',
29 | property: 'og:description',
30 | content: description,
31 | },
32 | {
33 | hid: 'og:image',
34 | property: 'og:image',
35 | content: '/og-image.jpg',
36 | },
37 | {
38 | hid: 'og:url',
39 | property: 'og:url',
40 | content: 'https://vue-global-loader.pages.dev',
41 | },
42 | {
43 | hid: 'twitter:title',
44 | name: 'twitter:title',
45 | content: `${siteName} - ${description}`,
46 | },
47 | {
48 | hid: 'twitter:description',
49 | name: 'twitter:description',
50 | content: description,
51 | },
52 |
53 | {
54 | hid: 'twitter:image',
55 | name: 'twitter:image',
56 | content: '/og-image.jpg',
57 | },
58 | {
59 | hid: 'twitter:card',
60 | name: 'twitter:card',
61 | content: 'summary_large_image',
62 | },
63 | ],
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/playground/utils/spinners.ts:
--------------------------------------------------------------------------------
1 | import {
2 | CircleSpinner,
3 | RingSpinner,
4 | RingDotSpinner,
5 | RingBarsSpinner,
6 | PulseSpinner,
7 | BarsSpinner,
8 | DotsSpinner,
9 | WaveSpinner,
10 | } from '#components'
11 |
12 | export const pkgSpinners = {
13 | CircleSpinner,
14 | RingSpinner,
15 | RingDotSpinner,
16 | RingBarsSpinner,
17 | PulseSpinner,
18 | BarsSpinner,
19 | DotsSpinner,
20 | WaveSpinner,
21 | }
22 |
23 | export function useStore() {
24 | return useNuxtApp().$store
25 | }
26 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - 'packages/*'
3 | - 'playground'
4 | - 'tests'
5 |
--------------------------------------------------------------------------------
/spa-loading-templates/bars-spinner.html:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
71 |
72 |
--------------------------------------------------------------------------------
/spa-loading-templates/circle-spinner.html:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
74 |
75 |
--------------------------------------------------------------------------------
/spa-loading-templates/dots-spinner.html:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
68 |
69 |
--------------------------------------------------------------------------------
/spa-loading-templates/pulse-spinner.html:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
58 |
59 |
--------------------------------------------------------------------------------
/spa-loading-templates/ring-bars-spinner.html:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
89 |
90 |
--------------------------------------------------------------------------------
/spa-loading-templates/ring-dot-spinner.html:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
57 |
58 |
--------------------------------------------------------------------------------
/spa-loading-templates/ring-spinner.html:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
60 |
61 |
--------------------------------------------------------------------------------
/spa-loading-templates/wave-spinner.html:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
223 |
224 |
--------------------------------------------------------------------------------
/svgs/bars-spinner.svg:
--------------------------------------------------------------------------------
1 |
35 |
--------------------------------------------------------------------------------
/svgs/circle-spinner.svg:
--------------------------------------------------------------------------------
1 |
41 |
--------------------------------------------------------------------------------
/svgs/dots-spinner.svg:
--------------------------------------------------------------------------------
1 |
48 |
--------------------------------------------------------------------------------
/svgs/pulse-spinner.svg:
--------------------------------------------------------------------------------
1 |
29 |
--------------------------------------------------------------------------------
/svgs/ring-bars-spinner.svg:
--------------------------------------------------------------------------------
1 |
62 |
--------------------------------------------------------------------------------
/svgs/ring-dot-spinner.svg:
--------------------------------------------------------------------------------
1 |
25 |
--------------------------------------------------------------------------------
/svgs/ring-spinner.svg:
--------------------------------------------------------------------------------
1 |
28 |
--------------------------------------------------------------------------------
/svgs/wave-spinner.svg:
--------------------------------------------------------------------------------
1 |
183 |
--------------------------------------------------------------------------------
/tests/cypress.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'cypress'
2 | import { resolve } from 'path'
3 | import vueJsx from '@vitejs/plugin-vue-jsx'
4 |
5 | import vue from '@vitejs/plugin-vue'
6 |
7 | export default defineConfig({
8 | video: false,
9 | viewportWidth: 1280,
10 | viewportHeight: 720,
11 | experimentalMemoryManagement: true,
12 | component: {
13 | devServer: {
14 | framework: 'vue',
15 | bundler: 'vite',
16 | viteConfig: {
17 | optimizeDeps: {
18 | include: ['vue-global-loader'],
19 | },
20 | server: {
21 | port: 5176,
22 | },
23 | resolve: {
24 | alias: {
25 | '@': resolve(__dirname, './'),
26 | },
27 | },
28 | plugins: [vue(), vueJsx()],
29 | },
30 | },
31 | },
32 | })
33 |
--------------------------------------------------------------------------------
/tests/cypress/support/commands.ts:
--------------------------------------------------------------------------------
1 | import { mount } from 'cypress/vue'
2 | import { createMemoryHistory, createRouter, type RouteRecordRaw } from 'vue-router'
3 | import tinycolor from 'tinycolor2'
4 |
5 | import { globalLoader, type GlobalLoaderOptions } from 'vue-global-loader'
6 |
7 | declare global {
8 | namespace Cypress {
9 | interface Chainable {
10 | mountApp(
11 | app: any,
12 | config?: Partial,
13 | routes?: RouteRecordRaw[]
14 | ): Chainable
15 | getRoot(): Chainable
16 | checkCssVars(config: Omit): Chainable
17 | checkComputedStyles(
18 | config: Omit
19 | ): Chainable
20 | checkDomAttrs(state: 'displayed' | 'destroyed'): Chainable
21 | triggerAppEvent(eventName: string): Chainable
22 | }
23 | }
24 | }
25 |
26 | Cypress.Commands.add(
27 | 'mountApp',
28 | (app: any, config: Partial = {}, routes = []) => {
29 | const router = createRouter({
30 | history: createMemoryHistory(),
31 | routes,
32 | })
33 |
34 | return mount(app, {
35 | global: {
36 | plugins: [
37 | {
38 | install(app) {
39 | app.use(globalLoader, config)
40 | app.use(router)
41 | },
42 | },
43 | ],
44 | stubs: {
45 | // https://github.com/vuejs/vue-test-utils/issues/890
46 | transition: false,
47 | },
48 | },
49 | })
50 | }
51 | )
52 |
53 | Cypress.Commands.add('getRoot', () => cy.get('[data-cy-loader]'))
54 |
55 | Cypress.Commands.add('checkCssVars', { prevSubject: 'element' }, (subject, config) => {
56 | cy.wrap(subject)
57 | .should('have.attr', 'style')
58 | .and('include', '--v-gl-bg-color: ' + config.backgroundColor)
59 | .and('include', '--v-gl-bg-opacity: ' + config.backgroundOpacity)
60 | .and('include', '--v-gl-bg-blur: ' + config.backgroundBlur)
61 | .and('include', '--v-gl-fg-color: ' + config.foregroundColor)
62 | .and('include', '--v-gl-t-dur: ' + config.transitionDuration)
63 | .and('include', '--v-gl-z: ' + config.zIndex)
64 |
65 | return cy.wrap(subject)
66 | })
67 |
68 | Cypress.Commands.add('checkComputedStyles', { prevSubject: 'element' }, (subject, config) => {
69 | let shouldPass = false
70 |
71 | cy.wrap(subject)
72 | .should('have.css', 'backdropFilter', `blur(${config.backgroundBlur}px)`)
73 | .and('have.css', 'zIndex', config.zIndex.toString())
74 | .within(() => {
75 | cy.get('svg')
76 | .invoke('css', ['stroke', 'fill'])
77 | .then(({ stroke, fill }) => {
78 | const configColor = tinycolor(config.foregroundColor)
79 |
80 | const cssFill = tinycolor(fill as unknown as string)
81 | const cssStroke = tinycolor(stroke as unknown as string)
82 |
83 | // Maybe there's a more coincise way to do this?
84 | shouldPass =
85 | tinycolor.equals(configColor, cssFill) || tinycolor.equals(cssStroke, configColor)
86 |
87 | if (!shouldPass) throw new Error('Computed SVG styles do not match')
88 | })
89 |
90 | cy.get('div:not([aria-live])')
91 | .should('have.css', 'opacity', config.backgroundOpacity.toString())
92 | .invoke('css', 'backgroundColor')
93 | .then((backgroundColor) => {
94 | const configColor = tinycolor(config.backgroundColor)
95 | const cssColor = tinycolor(backgroundColor as unknown as string)
96 |
97 | expect(tinycolor.equals(configColor, cssColor)).to.be.true
98 | })
99 | })
100 |
101 | return cy.wrap(subject)
102 | })
103 |
104 | Cypress.Commands.add('checkDomAttrs', (state: 'displayed' | 'destroyed') => {
105 | if (state === 'displayed') {
106 | cy.get('body')
107 | .should('have.attr', 'aria-hidden', 'true')
108 | .and('have.css', 'pointerEvents', 'none')
109 |
110 | cy.get('html').should('have.css', 'overflow', 'hidden')
111 | } else {
112 | cy.get('body')
113 | .should('not.have.attr', 'aria-hidden', 'true')
114 | .and('not.have.css', 'pointerEvents', 'none')
115 |
116 | cy.get('html').should('not.have.css', 'overflow', 'hidden')
117 | }
118 | })
119 |
120 | Cypress.Commands.add('triggerAppEvent', (eventName: string) => {
121 | cy.get('body').trigger(eventName, { force: true })
122 | })
123 |
--------------------------------------------------------------------------------
/tests/cypress/support/component-index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vue Global Loader Component Testing
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/tests/cypress/support/component.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | import './commands'
4 |
--------------------------------------------------------------------------------
/tests/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-global-loader-tests",
3 | "private": true,
4 | "scripts": {
5 | "test": "cypress run --component --browser chrome",
6 | "test:gui": "cypress open --component --browser chrome"
7 | },
8 | "dependencies": {
9 | "tinycolor2": "^1.6.0",
10 | "vue-global-loader": "workspace:*",
11 | "vue-router": "^4.3.2"
12 | },
13 | "devDependencies": {
14 | "@types/node": "^20.12.8",
15 | "@types/tinycolor2": "^1.4.6",
16 | "@vitejs/plugin-vue": "^5.0.4",
17 | "@vitejs/plugin-vue-jsx": "^3.1.0",
18 | "cypress": "^13.8.1",
19 | "typescript": "^5.4.5",
20 | "vite": "^5.2.11",
21 | "vue": "^3.4.26",
22 | "vue-tsc": "^2.0.16"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/tests/specs/callbacks.cy.tsx:
--------------------------------------------------------------------------------
1 | import { defineComponent, onMounted, ref } from 'vue'
2 | import { useGlobalLoader } from 'vue-global-loader'
3 |
4 | import GlobalLoader from 'vue-global-loader/GlobalLoader.vue'
5 | import CircleSpinner from 'vue-global-loader/CircleSpinner.vue'
6 |
7 | describe('Callbacks', () => {
8 | describe('onDestroyed', () => {
9 | const destroyedText = 'Destroyed'
10 |
11 | const App = defineComponent({
12 | setup() {
13 | const { displayLoader, destroyLoader } = useGlobalLoader()
14 | const { displayLoader: displayLoader2, destroyLoader: destroyLoader2 } =
15 | useGlobalLoader()
16 |
17 | const result = ref('')
18 |
19 | onMounted(() => {
20 | window.addEventListener('display-loader', () => displayLoader())
21 | window.addEventListener('destroy-loader', () =>
22 | destroyLoader(() => {
23 | result.value = destroyedText
24 | })
25 | )
26 |
27 | window.addEventListener('display-loader-2', () => {
28 | displayLoader2()
29 | result.value = ''
30 | })
31 | window.addEventListener('destroy-loader-2', () => destroyLoader2())
32 | })
33 |
34 | return () => (
35 | <>
36 | {result.value}
37 |
38 |
39 |
40 | >
41 | )
42 | },
43 | })
44 |
45 | function testOnDestroyed() {
46 | cy.triggerAppEvent('display-loader')
47 | cy.getRoot().should('exist')
48 |
49 | cy.triggerAppEvent('destroy-loader')
50 | cy.getRoot().should('not.exist')
51 |
52 | cy.get('[data-cy-callback]').should('contain.text', destroyedText)
53 | }
54 |
55 | it('onDestroyed callback is called', () => {
56 | cy.mountApp(App)
57 | testOnDestroyed()
58 | })
59 |
60 | it('onDestroyed callback is called if transition is disabled', () => {
61 | cy.mountApp(App, { transitionDuration: 0 })
62 | testOnDestroyed()
63 | })
64 |
65 | it('Previous onDestroyed callback is not available in a new context', () => {
66 | cy.mountApp(App)
67 |
68 | cy.triggerAppEvent('display-loader')
69 | cy.getRoot().should('exist')
70 |
71 | cy.triggerAppEvent('destroy-loader')
72 | cy.getRoot().should('not.exist')
73 |
74 | cy.triggerAppEvent('display-loader-2')
75 | cy.getRoot().should('exist')
76 |
77 | cy.triggerAppEvent('destroy-loader-2')
78 | cy.getRoot().should('not.exist')
79 |
80 | cy.get('[data-cy-callback]').should('not.contain.text', destroyedText)
81 | })
82 | })
83 |
84 | describe('onDisplayed', () => {
85 | const App = defineComponent({
86 | setup() {
87 | const { displayLoader } = useGlobalLoader()
88 |
89 | const result = ref(0)
90 |
91 | onMounted(() => {
92 | window.addEventListener('display-loader', async () => {
93 | const now = Date.now()
94 | await displayLoader()
95 | result.value = Date.now() - now
96 | })
97 | })
98 |
99 | return () => (
100 | <>
101 | {result.value}
102 |
103 |
104 |
105 | >
106 | )
107 | },
108 | })
109 |
110 | it('displayLoader returns a promise', () => {
111 | cy.mountApp(App, { transitionDuration: 1000 })
112 |
113 | cy.triggerAppEvent('display-loader')
114 | cy.get('[data-cy-promise]').should(($div) => {
115 | const duration = parseInt($div.text())
116 | expect(duration).to.be.greaterThan(1000).and.to.be.approximately(1000, 100)
117 | })
118 | })
119 |
120 | it('displayLoader resolves if transition is disabled', () => {
121 | cy.mountApp(App, { transitionDuration: 0 })
122 |
123 | cy.triggerAppEvent('display-loader')
124 | cy.get('[data-cy-promise]').should(($div) => {
125 | const duration = parseInt($div.text())
126 | expect(duration).to.be.greaterThan(0).to.be.approximately(0, 100)
127 | })
128 | })
129 | })
130 | })
131 |
--------------------------------------------------------------------------------
/tests/specs/config.cy.tsx:
--------------------------------------------------------------------------------
1 | import { defineComponent as c, onMounted } from 'vue'
2 | import { useGlobalLoader, DEFAULT_OPTIONS as DEF } from 'vue-global-loader'
3 |
4 | import GlobalLoader from 'vue-global-loader/GlobalLoader.vue'
5 | import CircleSpinner from 'vue-global-loader/CircleSpinner.vue'
6 |
7 | describe('Config', () => {
8 | const App = c({
9 | setup() {
10 | const { displayLoader } = useGlobalLoader()
11 | onMounted(displayLoader)
12 |
13 | return () => (
14 |
15 |
16 |
17 | )
18 | },
19 | })
20 |
21 | it('Default config is injected', () => {
22 | cy.mountApp(App)
23 | .getRoot()
24 | .checkCssVars(DEF)
25 | .checkComputedStyles(DEF)
26 |
27 | .get('[aria-live]')
28 | .should('contain.text', DEF.screenReaderMessage)
29 | })
30 |
31 | it('Custom config is injected', () => {
32 | const customConf = {
33 | backgroundColor: 'red',
34 | backgroundOpacity: 0.5,
35 | backgroundBlur: 10,
36 | foregroundColor: 'blue',
37 | transitionDuration: 1000,
38 | screenReaderMessage: 'Custom message',
39 | zIndex: 1000,
40 | }
41 |
42 | cy.mountApp(App, customConf)
43 | .getRoot()
44 | .checkCssVars(customConf)
45 | .checkComputedStyles(customConf)
46 |
47 | .get('[aria-live]')
48 | .should('contain.text', customConf.screenReaderMessage)
49 | })
50 |
51 | it('Custom config overrides and is merged with default config', () => {
52 | const customConf2 = {
53 | backgroundColor: 'red',
54 | backgroundOpacity: 0.5,
55 | backgroundBlur: 10,
56 | } as const
57 |
58 | cy.mountApp(App, customConf2)
59 | .getRoot()
60 | .checkCssVars({ ...DEF, ...customConf2 })
61 | .checkComputedStyles({ ...DEF, ...customConf2 })
62 |
63 | .get('[aria-live]')
64 | .should('contain.text', DEF.screenReaderMessage)
65 | })
66 |
67 | it('Config can be updated via `updateOptions`', () => {
68 | const customConf = {
69 | backgroundColor: 'orange',
70 | backgroundOpacity: 0.75,
71 | backgroundBlur: 5,
72 | screenReaderMessage: 'Custom updated message',
73 | }
74 |
75 | const App = c({
76 | setup() {
77 | const { displayLoader, updateOptions } = useGlobalLoader()
78 |
79 | onMounted(() => {
80 | window.addEventListener('display-loader', () => displayLoader())
81 | window.addEventListener('update-options', () => updateOptions(customConf))
82 | })
83 |
84 | return () => (
85 |
86 |
87 |
88 | )
89 | },
90 | })
91 |
92 | cy.mountApp(App)
93 | .triggerAppEvent('display-loader')
94 |
95 | .getRoot()
96 | .checkCssVars(DEF)
97 | .checkComputedStyles(DEF)
98 | .get('[aria-live]')
99 | .should('contain.text', DEF.screenReaderMessage)
100 |
101 | .triggerAppEvent('update-options')
102 |
103 | .getRoot()
104 | .checkCssVars({ ...DEF, ...customConf })
105 | .checkComputedStyles({ ...DEF, ...customConf })
106 | .get('[aria-live]')
107 | .should('contain.text', customConf.screenReaderMessage)
108 | })
109 | })
110 |
--------------------------------------------------------------------------------
/tests/specs/dom.cy.tsx:
--------------------------------------------------------------------------------
1 | import { defineComponent as c, onMounted, ref } from 'vue'
2 | import { RouterView, useRouter } from 'vue-router'
3 | import { useGlobalLoader } from 'vue-global-loader'
4 |
5 | import GlobalLoader from 'vue-global-loader/GlobalLoader.vue'
6 | import CircleSpinner from 'vue-global-loader/CircleSpinner.vue'
7 |
8 | describe('DOM Mutations', () => {
9 | const App = c({
10 | setup() {
11 | const { displayLoader, destroyLoader } = useGlobalLoader()
12 | const isMounted = ref(true)
13 |
14 | onMounted(() => {
15 | window.addEventListener('display-loader', () => displayLoader())
16 | window.addEventListener('destroy-loader', () => destroyLoader())
17 | window.addEventListener('destroy-global-loader', () => (isMounted.value = false))
18 | })
19 |
20 | return () =>
21 | isMounted.value && (
22 |
23 |
24 |
25 | )
26 | },
27 | })
28 |
29 | it('Teleports to HTML', () => {
30 | cy.mountApp(App)
31 |
32 | .get('body')
33 | .triggerAppEvent('display-loader')
34 |
35 | cy.get('body').siblings('[data-cy-loader]').should('exist')
36 | })
37 |
38 | function checkDom() {
39 | for (let i = 0; i < 20; i++) {
40 | cy.triggerAppEvent('display-loader')
41 | cy.getRoot().should('exist')
42 | cy.checkDomAttrs('displayed')
43 |
44 | cy.triggerAppEvent('destroy-loader')
45 | cy.getRoot().should('not.exist')
46 | cy.checkDomAttrs('destroyed')
47 | }
48 | }
49 |
50 | it('DOM mutations are toggled properly', () => {
51 | cy.mountApp(App)
52 |
53 | checkDom()
54 | })
55 |
56 | it('DOM mutations are toggled properly if transition is disabled', () => {
57 | cy.mountApp(App, { transitionDuration: 0 })
58 |
59 | checkDom()
60 | })
61 |
62 | it('DOM mutations are restored if GlobalLoader is removed from the DOM', () => {
63 | cy.mountApp(App)
64 |
65 | cy.triggerAppEvent('display-loader')
66 | cy.getRoot().should('exist')
67 | cy.checkDomAttrs('displayed')
68 |
69 | cy.triggerAppEvent('destroy-global-loader')
70 | cy.getRoot().should('not.exist')
71 | cy.checkDomAttrs('destroyed')
72 | })
73 | })
74 |
75 | describe('Focus', () => {
76 | const App = c({
77 | setup() {
78 | const router = useRouter()
79 | const { displayLoader, destroyLoader } = useGlobalLoader()
80 |
81 | onMounted(() => {
82 | window.addEventListener('display-loader', () => displayLoader())
83 | window.addEventListener('destroy-loader', () => destroyLoader())
84 | window.addEventListener('go-to-about', () => router.push('/about'))
85 | })
86 |
87 | return () => (
88 | <>
89 |
90 |
91 |
92 |
93 | >
94 | )
95 | },
96 | })
97 |
98 | const Home = c({
99 | setup() {
100 | return () => (
101 | <>
102 | Home
103 |
104 | >
105 | )
106 | },
107 | })
108 |
109 | const About = c({
110 | setup() {
111 | return () => About
112 | },
113 | })
114 |
115 | it('Focuses back to prev focused element', () => {
116 | cy.mountApp(App, {}, [
117 | {
118 | path: '/',
119 | component: Home,
120 | },
121 | ])
122 | .get('[data-cy-submit]')
123 | .focus()
124 |
125 | cy.focused().should('exist').and('have.attr', 'data-cy-submit')
126 |
127 | cy.triggerAppEvent('display-loader')
128 |
129 | cy.focused().should('not.exist')
130 |
131 | cy.triggerAppEvent('destroy-loader')
132 |
133 | cy.focused().should('exist').and('have.attr', 'data-cy-submit')
134 | })
135 |
136 | it("Doesn't throw if after navigation, prev focused element is not available", () => {
137 | cy.mountApp(App, {}, [
138 | { path: '/', component: Home },
139 | { path: '/about', component: About },
140 | ])
141 | .get('[data-cy-submit]')
142 | .focus()
143 |
144 | cy.focused().should('exist').and('have.attr', 'data-cy-submit')
145 |
146 | cy.triggerAppEvent('display-loader')
147 | cy.triggerAppEvent('go-to-about')
148 | cy.triggerAppEvent('destroy-loader')
149 |
150 | cy.get('h1').should('contain.text', 'About')
151 |
152 | cy.focused().should('not.exist')
153 | })
154 | })
155 |
--------------------------------------------------------------------------------
/tests/specs/router.cy.tsx:
--------------------------------------------------------------------------------
1 | import { defineComponent as c, onMounted } from 'vue'
2 | import { RouterView, useRouter } from 'vue-router'
3 | import { useGlobalLoader } from 'vue-global-loader'
4 |
5 | import GlobalLoader from 'vue-global-loader/GlobalLoader.vue'
6 | import CircleSpinner from 'vue-global-loader/CircleSpinner.vue'
7 |
8 | describe('Router', () => {
9 | const NAVIGATION_DELAY = 2000
10 |
11 | const App = c({
12 | setup() {
13 | return () => (
14 | <>
15 |
16 |
17 |
18 |
19 | >
20 | )
21 | },
22 | })
23 |
24 | const Home = c({
25 | setup() {
26 | const { displayLoader } = useGlobalLoader()
27 | const onDisplay = () => displayLoader()
28 |
29 | const router = useRouter()
30 |
31 | onMounted(() => {
32 | window.addEventListener('display-loader', onDisplay)
33 |
34 | setTimeout(() => {
35 | router.push('/about')
36 | }, NAVIGATION_DELAY)
37 | })
38 |
39 | return () => Home
40 | },
41 | })
42 |
43 | const About = c({
44 | setup() {
45 | const { destroyLoader } = useGlobalLoader()
46 | const onDestroy = () => destroyLoader()
47 |
48 | onMounted(() => window.addEventListener('destroy-loader', onDestroy))
49 |
50 | return () => About
51 | },
52 | })
53 |
54 | it('Loader persists and can be destroyed after navigation', () => {
55 | cy.mountApp(App, {}, [
56 | { path: '/', component: Home },
57 | { path: '/about', component: About },
58 | ])
59 |
60 | cy.triggerAppEvent('display-loader')
61 |
62 | cy.wait(NAVIGATION_DELAY)
63 |
64 | cy.get('h1')
65 | .should('contain.text', 'About')
66 | .getRoot()
67 | .should('be.visible')
68 | .triggerAppEvent('destroy-loader')
69 |
70 | cy.getRoot().should('not.exist')
71 | })
72 | })
73 |
--------------------------------------------------------------------------------
/tests/specs/scoped-options.cy.tsx:
--------------------------------------------------------------------------------
1 | import { defineComponent, onMounted } from 'vue'
2 | import { useGlobalLoader, DEFAULT_OPTIONS as DEF } from 'vue-global-loader'
3 |
4 | import GlobalLoader from 'vue-global-loader/GlobalLoader.vue'
5 | import CircleSpinner from 'vue-global-loader/CircleSpinner.vue'
6 |
7 | describe('Scoped Options', () => {
8 | const customConf = {
9 | backgroundColor: 'red',
10 | backgroundOpacity: 0.5,
11 | backgroundBlur: 10,
12 | foregroundColor: 'blue',
13 | transitionDuration: 1000,
14 | screenReaderMessage: 'Custom message',
15 | zIndex: 1000,
16 | }
17 |
18 | const App = defineComponent({
19 | setup() {
20 | const { displayLoader, destroyLoader } = useGlobalLoader(customConf)
21 | const { displayLoader: displayLoader2 } = useGlobalLoader()
22 |
23 | onMounted(() => {
24 | window.addEventListener('display-loader', () => displayLoader())
25 | window.addEventListener('destroy-loader', () => destroyLoader())
26 | window.addEventListener('display-loader-2', () => displayLoader2())
27 | })
28 |
29 | return () => (
30 |
31 |
32 |
33 | )
34 | },
35 | })
36 |
37 | it('Scoped options are applied to a specific loader and restored on destroy', () => {
38 | cy.mountApp(App)
39 |
40 | cy.triggerAppEvent('display-loader')
41 |
42 | cy.getRoot()
43 | .checkCssVars(customConf)
44 | .checkComputedStyles(customConf)
45 | .within(() => {
46 | cy.get('[aria-live]').should('contain.text', customConf.screenReaderMessage)
47 | })
48 |
49 | cy.triggerAppEvent('destroy-loader')
50 |
51 | cy.getRoot().should('not.exist')
52 | cy.triggerAppEvent('display-loader-2')
53 | cy.getRoot()
54 | .checkCssVars(DEF)
55 | .checkComputedStyles(DEF)
56 | .within(() => {
57 | cy.get('[aria-live]').should('contain.text', DEF.screenReaderMessage)
58 | })
59 | })
60 | })
61 |
--------------------------------------------------------------------------------
/tests/specs/spinners.cy.tsx:
--------------------------------------------------------------------------------
1 | import { defineComponent, onMounted } from 'vue'
2 | import { useGlobalLoader } from 'vue-global-loader'
3 |
4 | import GlobalLoader from 'vue-global-loader/GlobalLoader.vue'
5 | import CircleSpinner from 'vue-global-loader/CircleSpinner.vue'
6 | import RingSpinner from 'vue-global-loader/RingSpinner.vue'
7 | import RingDotSpinner from 'vue-global-loader/RingDotSpinner.vue'
8 | import RingBarsSpinner from 'vue-global-loader/RingBarsSpinner.vue'
9 | import PulseSpinner from 'vue-global-loader/PulseSpinner.vue'
10 | import BarsSpinner from 'vue-global-loader/BarsSpinner.vue'
11 | import DotsSpinner from 'vue-global-loader/DotsSpinner.vue'
12 | import WaveSpinner from 'vue-global-loader/WaveSpinner.vue'
13 |
14 | describe('Spinners', () => {
15 | it('All spinners are rendered', () => {
16 | ;[
17 | CircleSpinner,
18 | RingSpinner,
19 | RingDotSpinner,
20 | RingBarsSpinner,
21 | PulseSpinner,
22 | BarsSpinner,
23 | DotsSpinner,
24 | WaveSpinner,
25 | ].forEach((Spinner) => {
26 | const app = defineComponent({
27 | setup() {
28 | const { displayLoader } = useGlobalLoader()
29 | onMounted(displayLoader)
30 |
31 | return () => (
32 |
33 |
34 |
35 | )
36 | },
37 | })
38 |
39 | cy.mountApp(app)
40 | .getRoot()
41 | .within(() => {
42 | cy.get('svg').should('exist')
43 | })
44 | })
45 | })
46 | })
47 |
--------------------------------------------------------------------------------
/tests/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2018",
4 | "module": "ESNext",
5 | "moduleResolution": "Node",
6 | "strict": true,
7 | "resolveJsonModule": true,
8 | "isolatedModules": true,
9 | "esModuleInterop": true,
10 | "lib": ["ESNext", "DOM"],
11 | "skipLibCheck": true,
12 | "jsx": "preserve",
13 | "jsxImportSource": "vue",
14 | "noEmit": true,
15 | "baseUrl": ".",
16 | "paths": {
17 | "@/support/*": ["./cypress/support/*"],
18 | "@/*": ["./*"]
19 | }
20 | },
21 | "include": ["."]
22 | }
23 |
--------------------------------------------------------------------------------