diff --git a/.prettierignore b/.prettierignore index 1521c8b..a60030e 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1 +1,2 @@ -dist +dist/ +coverage/ diff --git a/README.md b/README.md index f2f38c6..050fc39 100644 --- a/README.md +++ b/README.md @@ -228,9 +228,9 @@ npm run test ### Unit -Jest is used for unit-tests. +Vitest is used for unit-tests. -[`Jest`](https://jestjs.io) and [`VueTestUtils`](https://vue-test-utils.vuejs.org) is used for unit tests. +[`Vitest`](https://vitest.dev/) and [`VueTestUtils`](https://test-utils.vuejs.org) are used for unit tests. You can run unit tests by running the next command: @@ -267,8 +267,8 @@ To build the production ready bundle simply run `npm run build`: After the successful build the following files will be generated in the `dist` folder: ``` -├── vue-lazy-youtube-video.common.js -├── vue-lazy-youtube-video.esm.js +├── vue-lazy-youtube-video.cjs.js +├── vue-lazy-youtube-video.mjs ├── vue-lazy-youtube-video.js ├── vue-lazy-youtube-video.min.js ├── style.css @@ -280,10 +280,9 @@ After the successful build the following files will be generated in the `dist` f ## Powered by - [`Vue`](https://vuejs.org) -- [`Rollup`](https://rollupjs.org/guide/en) (and plugins) -- [`Babel`](https://babeljs.io) -- [`Jest`](https://jestjs.io) -- [`Vue Test Utils`](http://vue-test-utils.vuejs.org) +- [`Vite`](https://vitejs.dev/) (and plugins) +- [`Vitest`](https://vitest.dev/) +- [`Vue Test Utils`](https://test-utils.vuejs.org/) - [`TypeScript`](http://www.typescriptlang.org) - [`PostCSS`](https://postcss.org) diff --git a/build/base/plugins/index.js b/build/base/plugins/index.js deleted file mode 100644 index 0e93f64..0000000 --- a/build/base/plugins/index.js +++ /dev/null @@ -1,7 +0,0 @@ -import common from '@rollup/plugin-commonjs' -import resolve from '@rollup/plugin-node-resolve' - -/** @type {import('rollup').RollupOptions['plugins']} */ -const plugins = [common(), resolve()] - -export default plugins diff --git a/build/rollup.config.dev.js b/build/rollup.config.dev.js deleted file mode 100644 index 9ed2cd6..0000000 --- a/build/rollup.config.dev.js +++ /dev/null @@ -1,38 +0,0 @@ -import path from 'path' -import { defineConfig } from 'rollup' -import typescript from '@rollup/plugin-typescript' -import serve from 'rollup-plugin-serve' -import livereload from 'rollup-plugin-livereload' -import replace from '@rollup/plugin-replace' -import css from 'rollup-plugin-css-only' - -import plugins from './base/plugins/index' - -const DEMO_DIR = path.join(__dirname, '../demo') - -export default defineConfig({ - input: path.join(DEMO_DIR, 'index.ts'), - output: { - file: path.join(DEMO_DIR, 'demo.js'), - format: 'iife', - name: 'demo', - sourcemap: true, - }, - plugins: [ - typescript(), - css(), - replace({ - 'process.env.NODE_ENV': JSON.stringify('development'), - preventAssignment: true, - }), - serve({ - open: true, - contentBase: DEMO_DIR, - port: 8080, - }), - livereload({ - verbose: true, - watch: DEMO_DIR, - }), - ].concat(plugins), -}) diff --git a/build/rollup.config.prod.js b/build/rollup.config.prod.js deleted file mode 100644 index 16aa341..0000000 --- a/build/rollup.config.prod.js +++ /dev/null @@ -1,69 +0,0 @@ -import path from 'path' -import { defineConfig } from 'rollup' -import typescript from '@rollup/plugin-typescript' -import replace from '@rollup/plugin-replace' -import { terser } from 'rollup-plugin-terser' - -import basePlugins from './base/plugins/index' - -const SOURCE = path.join(__dirname, '../src/index.ts') -const DIST_DIR = 'dist' -const FILE_NAME = 'vue-lazy-youtube-video' -const name = 'VueLazyYoutubeVideo' -const external = ['vue'] -const globals = { - vue: 'Vue', -} -const plugins = [ - replace({ - 'process.env.NODE_ENV': JSON.stringify('production'), - preventAssignment: true, - }), -].concat(basePlugins) - -export default [ - defineConfig({ - input: SOURCE, - external, - output: [ - { - file: `${DIST_DIR}/${FILE_NAME}.js`, - format: 'umd', - exports: 'named', - globals, - name, - }, - { - file: `${DIST_DIR}/${FILE_NAME}.common.js`, - exports: 'named', - format: 'cjs', - }, - { - file: `${DIST_DIR}/${FILE_NAME}.esm.js`, - format: 'esm', - }, - ], - plugins: [ - typescript({ - tsconfig: './tsconfig.prod.json', - }), - ].concat(plugins), - }), - defineConfig({ - input: SOURCE, - external, - output: { - file: `${DIST_DIR}/${FILE_NAME}.min.js`, - format: 'umd', - exports: 'named', - globals, - name, - }, - plugins: [ - typescript({ - tsconfig: './tsconfig.prod.umd.json', - }), - terser({ output: { comments: false } }), - ].concat(plugins), - }), -] diff --git a/demo/App.ts b/demo/App.ts deleted file mode 100644 index 02a3c32..0000000 --- a/demo/App.ts +++ /dev/null @@ -1,86 +0,0 @@ -import Vue, { VNode } from 'vue' - -import { LoadIframeEventPayload } from '../src/types' -import VueLazyYoutubeVideo from '../src' - -export default Vue.extend({ - name: 'AppPage', - data() { - return { - thumbnailListeners: { - load() { - console.log('load') - }, - }, - iframeAttributes: { - id: 'foo', - }, - } - }, - methods: { - onIframeLoad({ iframe }: LoadIframeEventPayload) { - console.log(' + + + + + + + + diff --git a/src/constants/index.ts b/src/constants/index.ts index 5292918..7d17f76 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -21,7 +21,8 @@ export const DEFAULT_IFRAME_ATTRIBUTES = { 'accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture', } -export const YOUTUBE_REGEX = /^https:\/\/www\.youtube(?:-nocookie)?\.com\/embed\/(.+?)(?:\?.*)?$/ +export const YOUTUBE_REGEX = + /^https:\/\/www\.youtube(?:-nocookie)?\.com\/embed\/(.+?)(?:\?.*)?$/ export const PLAYER_SCRIPT_SRC = 'https://www.youtube.com/player_api' diff --git a/src/event/index.ts b/src/event/index.ts deleted file mode 100644 index 032e3b3..0000000 --- a/src/event/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum Event { - LOAD_IFRAME = 'load:iframe', - INIT_PLAYER = 'init:player', -} diff --git a/src/helpers/index.ts b/src/helpers/index.ts index 1c3d7b0..fc4b6fd 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -1,14 +1,9 @@ -/** - * @see https://stackoverflow.com/a/30867255/11761617 - */ -export function startsWith( - string: string, - value: string, - position: number = 0 -) { - return string.indexOf(value, position) === position -} +export const isAspectRatio = (value: string) => /^\d+:\d+$/.test(value) -export function isAspectRatio(value: string) { - return /^\d+:\d+$/.test(value) -} +export const toListenersWithOn = (listeners: Record) => + Object.fromEntries( + Object.entries(listeners).map(([key, value]) => [ + `on${key[0].toUpperCase()}${key.slice(1)}`, + value, + ]) + ) diff --git a/src/index.ts b/src/index.ts index 490df54..5d51c23 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,10 @@ -import VueLazyYoutubeVideo from './VueLazyYoutubeVideo' - -import { PluginObject } from 'vue' +import VueLazyYoutubeVideo from './VueLazyYoutubeVideo.vue' +import { App, Plugin } from 'vue' export default VueLazyYoutubeVideo -export const Plugin: PluginObject = { - install(Vue) { - Vue.component('LazyYoutubeVideo', VueLazyYoutubeVideo) +export const plugin: Plugin = { + install: (app: App) => { + app.component('LazyYoutubeVideo', VueLazyYoutubeVideo) }, } diff --git a/src/types.ts b/src/types.ts index 3894d58..dbeedc0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,22 +1,11 @@ -import type { Event } from './event' - -export interface Events { - [Event.LOAD_IFRAME]: LoadIframeEventPayload - [Event.INIT_PLAYER]: InitPlayerEventPayload -} - export interface LoadIframeEventPayload { - iframe?: HTMLIFrameElement + iframe: HTMLIFrameElement | null } export interface InitPlayerEventPayload { instance: YT.Player } -export type Refs = { - iframe?: HTMLIFrameElement -} - export interface Thumbnail { webp: string jpg: string diff --git a/test/fixtures/index.ts b/test/fixtures/index.ts index 6e0183b..48c91d9 100644 --- a/test/fixtures/index.ts +++ b/test/fixtures/index.ts @@ -1,5 +1,3 @@ -import cloneDeep from 'lodash.clonedeep' - export const VIDEO_ID = '4JS70KB9GS0' export const defaultProps = { @@ -7,7 +5,7 @@ export const defaultProps = { } export function getDefaultProps({ query }: { query?: string } = {}) { - const props = cloneDeep(defaultProps) + const props = { ...defaultProps } if (query) { props.src += query diff --git a/test/helpers/index.ts b/test/helpers/index.ts index 0c00563..5caa410 100644 --- a/test/helpers/index.ts +++ b/test/helpers/index.ts @@ -1,83 +1,58 @@ -import type Vue from 'vue' -import type { ThisTypedComponentOptionsWithRecordProps } from 'vue/types/options' -import type { Wrapper, ThisTypedShallowMountOptions } from '@vue/test-utils' -import { shallowMount } from '@vue/test-utils' +import { VueWrapper, mount } from '@vue/test-utils' import type { Props } from '../../types' -import VueLazyYoutubeVideo from '../../src/VueLazyYoutubeVideo' +import VueLazyYoutubeVideo from '../../src/VueLazyYoutubeVideo.vue' import { PLAYER_SCRIPT_SRC } from '../../src/constants' import { defaultProps } from '../fixtures' +import { vi, Mock } from 'vitest' -type ComponentInstance = InstanceType -type LocalWrapper = Wrapper - -declare global { - namespace NodeJS { - interface Global { - YT?: { - Player?: jest.Mock - } - } - } -} +type LocalWrapper = VueWrapper> export class TestManager { static createWrapper( - options?: ThisTypedShallowMountOptions & { - propsData?: Partial - } + options?: Record & { props?: Partial } ) { - return shallowMount(VueLazyYoutubeVideo, { + return mount(VueLazyYoutubeVideo, { ...options, - propsData: { + props: { ...defaultProps, - ...(options !== undefined - ? options.propsData !== undefined - ? options.propsData - : {} - : {}), + ...options?.props, }, }) } static async clickAndGetIframe(wrapper: LocalWrapper) { - wrapper.trigger('click') - await wrapper.vm.$nextTick() + await wrapper.trigger('click') return wrapper.find('iframe') } static async play(wrapper: LocalWrapper) { - wrapper.trigger('click') - await wrapper.vm.$nextTick() + await wrapper.trigger('click') const iframe = wrapper.find('iframe') iframe.trigger('load') } static mockGlobalPlayer() { global.YT = { - Player: jest.fn(), - } + Player: vi.fn(), + } as any } static getMockedPlayer() { const { YT } = global - if (YT !== undefined && YT.Player !== undefined) { - return YT.Player + if (YT?.Player !== undefined) { + return YT.Player as Mock } throw new Error('Unable to get mocked player') } static cleanGlobalPlayer() { - global.YT = undefined + global.YT = undefined as any } static getPropDefinition(key: K) { - const { props } = (VueLazyYoutubeVideo as typeof VueLazyYoutubeVideo & { - options: ThisTypedComponentOptionsWithRecordProps - }).options - if (props === undefined) throw new Error() - return props[key] + return VueLazyYoutubeVideo.props[key] } static getImgAndSourceElements(wrapper: LocalWrapper) { @@ -95,9 +70,9 @@ export class TestManager { } static getScriptElement() { - return document.querySelector( + return document.querySelector( `script[src="${PLAYER_SCRIPT_SRC}"]` - ) as HTMLScriptElement | null + ) } static cleanScriptElement() { diff --git a/test/index.test.ts b/test/index.test.ts index f959f01..6a30fe6 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -7,8 +7,7 @@ import { PREVIEW_IMAGE_SIZES, DEFAULT_PREVIEW_IMAGE_SIZE, } from '../src/constants' -import { Event } from '../src/event' - +import { it, describe, beforeEach, afterEach, expect, vi } from 'vitest' import { classes } from './config' import { defaultProps, getDefaultProps, VIDEO_ID } from './fixtures' import { TestManager } from './helpers' @@ -37,7 +36,7 @@ describe('VueLazyYoutubeVideo', () => { ) const query = '?loop=1' wrapper = TestManager.createWrapper({ - propsData: getDefaultProps({ query }), + props: getDefaultProps({ query }), }) iframe = await TestManager.clickAndGetIframe(wrapper) expect(iframe.element.getAttribute('src')).toBe( @@ -45,30 +44,21 @@ describe('VueLazyYoutubeVideo', () => { ) }) - it('should be `String`, required and be validated', (done) => { + it('should be `String`, required and be validated', () => { const definition = TestManager.getPropDefinition('src') expect(definition).toMatchObject({ type: String, required: true, }) - - if (typeof definition === 'object' && !Array.isArray(definition)) { - const { validator } = definition - if (validator) { - expect(validator('INVALID_VALUE')).toBeFalsy() - expect( - validator('https://www.youtube.com/embed/4JS70KB9GS0') - ).toBeTruthy() - expect( - validator('https://www.youtube-nocookie.com/embed/4JS70KB9GS0') - ).toBeTruthy() - done() - } else { - done.fail('Invalid prop definition') - } - } else { - done.fail('Invalid prop definition') - } + ;( + [ + ['INVALID_VALUE', false], + ['https://www.youtube.com/embed/4JS70KB9GS0', true], + ['https://www.youtube-nocookie.com/embed/4JS70KB9GS0', true], + ] as const + ).forEach(([value, isValid]) => { + expect(definition.validator(value)).toBe(isValid) + }) }) }) @@ -84,7 +74,7 @@ describe('VueLazyYoutubeVideo', () => { it(`should correctly set \`padding bottom\` of the \`\` when valid value is passed`, () => { const [a, b] = [4, 3] const wrapper = TestManager.createWrapper({ - propsData: { aspectRatio: `${a}:${b}` }, + props: { aspectRatio: `${a}:${b}` }, }) expect( (wrapper.find(classes.inner).element as HTMLElement).style @@ -96,7 +86,7 @@ describe('VueLazyYoutubeVideo', () => { const invalid = ['foo'] invalid.forEach((value) => { const wrapper = TestManager.createWrapper({ - propsData: { aspectRatio: value }, + props: { aspectRatio: value }, }) expect( (wrapper.find(classes.inner).element as HTMLElement).style @@ -111,18 +101,14 @@ describe('VueLazyYoutubeVideo', () => { type: String, default: DEFAULT_ASPECT_RATIO, }) - - if (typeof definition === 'object' && !Array.isArray(definition)) { - const { validator } = definition - if (validator) { - expect(validator('INVALID_VALUE')).toBeFalsy() - expect(validator('4:3')).toBeTruthy() - } else { - throw new Error() - } - } else { - throw new Error() - } + ;( + [ + ['INVALID_VALUE', false], + ['4:3', true], + ] as const + ).forEach(([value, isValid]) => { + expect(definition.validator(value)).toBe(isValid) + }) }) }) @@ -130,7 +116,7 @@ describe('VueLazyYoutubeVideo', () => { it('should correctly set `alt` attribute of the preview `` when valid value is passed', () => { const alt = 'foo' const wrapper = TestManager.createWrapper({ - propsData: { alt }, + props: { alt }, }) expect(wrapper.find('img').element.getAttribute('alt')).toBe(alt) }) @@ -162,7 +148,7 @@ describe('VueLazyYoutubeVideo', () => { it('should correctly set `aria-label` attribute of the `` when valid value is passed', () => { const buttonLabel = 'Simple dummy text' const wrapper = TestManager.createWrapper({ - propsData: { buttonLabel }, + props: { buttonLabel }, }) expect( wrapper.find(classes.button).element.getAttribute('aria-label') @@ -179,10 +165,10 @@ describe('VueLazyYoutubeVideo', () => { }) describe('previewImageSize', () => { - it('should correctly set `srcset` and `src` attributes of `` and `` when valid value is passed', (done) => { + it('should correctly set `srcset` and `src` attributes of `` and `` when valid value is passed', () => { const previewImageSize = PREVIEW_IMAGE_SIZES[0] const wrapper = TestManager.createWrapper({ - propsData: { + props: { previewImageSize, }, }) @@ -190,22 +176,12 @@ describe('VueLazyYoutubeVideo', () => { const srcAttribute = img.getAttribute('src') const srcsetAttribute = source.getAttribute('srcset') - if (srcAttribute !== null) { - expect(srcAttribute).toBe( - `https://i.ytimg.com/vi/${VIDEO_ID}/${previewImageSize}.jpg` - ) - } else { - done.fail('Invalid `src` attribute') - } - if (srcsetAttribute !== null) { - expect(srcsetAttribute).toBe( - `https://i.ytimg.com/vi_webp/${VIDEO_ID}/${previewImageSize}.webp` - ) - } else { - done.fail('Invalid `srcset` attribute') - } - - done() + expect(srcAttribute).toBe( + `https://i.ytimg.com/vi/${VIDEO_ID}/${previewImageSize}.jpg` + ) + expect(srcsetAttribute).toBe( + `https://i.ytimg.com/vi_webp/${VIDEO_ID}/${previewImageSize}.webp` + ) }) it('should correctly set `srcset` and `src` attributes of `` and `` when no value is passed', () => { @@ -227,55 +203,37 @@ describe('VueLazyYoutubeVideo', () => { } }) - it('should be `String` have default value and be validated', (done) => { + it('should be `String` have default value and be validated', () => { const definition = TestManager.getPropDefinition('previewImageSize') expect(definition).toMatchObject({ type: String, default: 'maxresdefault', }) - - if (typeof definition === 'object' && !Array.isArray(definition)) { - const { validator } = definition - if (validator) { - expect(validator('INVALID_VALUE')).toBeFalsy() - expect(validator(PREVIEW_IMAGE_SIZES[0])).toBeTruthy() - } else { - done.fail('Invalid validator definition') - } - } else { - done.fail('Invalid validator definition') - } - - done() + ;( + [ + ['INVALID_VALUE', false], + [PREVIEW_IMAGE_SIZES[0], true], + ] as const + ).forEach(([value, isValid]) => { + expect(definition.validator(value)).toBe(isValid) + }) }) }) describe('thumbnail', () => { it('should correctly set `srcset` and `src` attributes of thumbnails when valid value is passed', () => { const thumbnail = { webp: 'w', jpg: 'j' } - const wrapper = TestManager.createWrapper({ propsData: { thumbnail } }) + const wrapper = TestManager.createWrapper({ props: { thumbnail } }) const { img, source } = TestManager.getImgAndSourceElements(wrapper) expect(source.getAttribute('srcset')).toEqual(thumbnail.webp) expect(img.getAttribute('src')).toEqual(thumbnail.jpg) }) - it('should be `Object` and be validated', (done) => { + it('should be `Object` and be validated', () => { const definition = TestManager.getPropDefinition('thumbnail') expect(definition).toMatchObject({ type: Object, }) - - if (typeof definition === 'object' && !Array.isArray(definition)) { - const { validator } = definition - if (validator) { - expect(validator({ jpg: 'j', webp: 'w' })).toBeTruthy() - done() - } else { - done.fail('Invalid validator definition') - } - } else { - done.fail('Invalid validator definition') - } }) }) @@ -283,7 +241,7 @@ describe('VueLazyYoutubeVideo', () => { it('should correctly set attributes of the `