switch to playwright test runner, use esbuild for dev builds, switch to array destructuring

This commit is contained in:
Rene
2022-06-10 10:55:51 +02:00
parent 9d0dd41d7f
commit 35868511ff
156 changed files with 6693 additions and 7793 deletions
+17 -16
View File
@@ -1,8 +1,12 @@
const resolve = require('./resolve.config');
const browserRollupConfig = require('./config/jest-puppeteer.rollup.config.js');
module.exports = {
extends: ['plugin:jest-playwright/recommended', 'plugin:@typescript-eslint/recommended', 'plugin:react/recommended', 'airbnb', 'prettier'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'airbnb',
'prettier',
],
env: {
browser: true,
es2020: true,
@@ -43,6 +47,13 @@ module.exports = {
'consistent-return': 'off',
'import/prefer-default-export': 'off',
'import/no-extraneous-dependencies': 'off',
'import/no-unresolved': [
'error',
{
ignore: [`^@/.*`],
},
],
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
@@ -54,12 +65,6 @@ module.exports = {
allowedNames: ['self', '_self'], // Allow `const self = this`; `[]` by default
},
],
'import/no-unresolved': [
'error',
{
ignore: [`^@/.*`],
},
],
'import/extensions': [
'error',
'ignorePackages',
@@ -73,8 +78,10 @@ module.exports = {
},
overrides: [
{
files: ['*.test.*', `*${browserRollupConfig.js.input}.*`],
files: ['*.test.*', '**/tests/**'],
rules: {
'no-shadow': 'off',
'no-use-before-define': 'off',
'no-restricted-syntax': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/no-empty-function': 'off',
@@ -87,16 +94,10 @@ module.exports = {
'no-void': 'off',
'no-empty-function': 'off',
'no-new-func': 'off',
'import/no-unresolved': [
'error',
{
ignore: [`\\./${browserRollupConfig.build}/${browserRollupConfig.html.output}$`, `^@/.*`],
},
],
},
},
{
files: ['rollup.config.*'],
files: ['*rollup*'],
rules: {
'no-console': 'off',
'global-require': 'off',
+1 -1
View File
@@ -1,5 +1,5 @@
{
"printWidth": 150,
"printWidth": 100,
"tabWidth": 2,
"singleQuote": true,
"trailingComma": "es5",
+4 -12
View File
@@ -1,21 +1,13 @@
module.exports = function (api) {
api.cache.using(() => process.env.NODE_ENV);
const isRollup = api.caller((caller) => !!(caller && caller.name === 'babel-rollup-build'));
const isJest = api.caller((caller) => !!(caller && caller.name === 'babel-jest'));
if (isRollup) {
return {
plugins: [
'@babel/plugin-transform-runtime',
'@babel/plugin-proposal-class-properties',
['@babel/plugin-proposal-private-methods', { loose: false }],
],
};
}
if (isJest) {
return {
plugins: ['@babel/plugin-transform-modules-commonjs', ['@babel/plugin-proposal-private-methods', { loose: false }]],
plugins: [
'@babel/plugin-transform-modules-commonjs',
['@babel/plugin-proposal-private-methods', { loose: false }],
],
presets: [
[
'@babel/preset-env',
-32
View File
@@ -1,32 +0,0 @@
const PlaywrightEnvironment = require('jest-playwright-preset/lib/PlaywrightEnvironment').default;
const { setupRollupTest, cleanupRollupTest } = require('./jest-browser.rollup.js');
const buildTests = [];
class BrowserRollupEnvironment extends PlaywrightEnvironment {
constructor(envConfig, envContext) {
super(envConfig, envContext);
this.watch = (envConfig.displayName.name || '').includes('-dev');
this.ctx = envContext;
this.cfg = envConfig;
}
async setup() {
const { testPath } = this.ctx;
if (!buildTests.includes(testPath)) {
await cleanupRollupTest(testPath, this.cfg.cache);
await setupRollupTest(this.cfg.rootDir, this.ctx.testPath, this.cfg.cache && this.cfg.cacheDirectory, this.watch);
buildTests.push(testPath);
}
await super.setup();
}
async teardown() {
await super.teardown();
}
}
module.exports = BrowserRollupEnvironment;
-5
View File
@@ -1,5 +0,0 @@
const { globalSetup } = require('jest-playwright-preset');
module.exports = async (jestConfig) => {
await globalSetup(jestConfig);
};
-36
View File
@@ -1,36 +0,0 @@
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const del = require('del');
const { globalTeardown } = require('jest-playwright-preset');
const coverageTempDir = './.nyc_output';
const coverageTempDirFile = 'coverage.json';
const reportDir = './.coverage/browser';
module.exports = async (jestConfig) => {
await globalTeardown(jestConfig);
const { rootDir } = jestConfig;
const coverageTempDirPath = path.resolve(rootDir, coverageTempDir);
const coverageTempFilePath = path.resolve(coverageTempDirPath, coverageTempDirFile);
const reportDirPath = path.resolve(rootDir, reportDir);
if (fs.existsSync(coverageTempFilePath)) {
const coverageReportText = ' COVERAGE ';
console.log('');
console.log(`\x1b[1m\x1b[44m${coverageReportText}\x1b[0m`);
console.log(`Reporting from: "${path.relative(rootDir, coverageTempFilePath)}" in "${path.relative(rootDir, reportDirPath)}"`);
del.sync(reportDirPath);
execSync(`npx nyc report --reporter=lcov --report-dir=${reportDir}`, {
cwd: rootDir,
});
const [deletedTempDir] = del.sync(coverageTempDirPath);
if (deletedTempDir) {
console.log('Deleted:', path.relative(rootDir, deletedTempDir));
}
}
};
-19
View File
@@ -1,19 +0,0 @@
const path = require('path');
module.exports = {
port: 8080,
root: path.join(__dirname, '../'),
build: '.build',
html: {
input: 'index.html',
output: 'build.html',
},
js: {
input: 'index.browser',
output: 'build',
},
dev: {
servePort: 18080,
livereloadPort: 28080,
},
};
-337
View File
@@ -1,337 +0,0 @@
const fs = require('fs');
const crypto = require('crypto');
const path = require('path');
const del = require('del');
const chalk = require('chalk');
const readline = require('readline');
const rollup = require('rollup');
const rollupPluginHtml = require('@rollup/plugin-html');
const rollupPluginStyles = require('rollup-plugin-styles');
const rollupPluginServe = require('rollup-plugin-serve');
const rollupPluginLivereload = require('rollup-plugin-livereload');
const deploymentConfig = require('./jest-browser.rollup.config.js');
const rollupConfigName = 'rollup.config.js';
const cacheFilePrefix = 'jest-browser-overlayscrollbars-cache-';
const cacheEncoding = 'utf8';
const cacheHash = 'md5';
const rollupAdditionalWatchFiles = (files) => ({
buildStart() {
if (files) {
files.forEach((file) => {
if (fs.existsSync(file)) {
this.addWatchFile(file);
}
});
}
},
});
const makeHtmlAttributes = (attributes) => {
if (!attributes) {
return '';
}
const keys = Object.keys(attributes);
// eslint-disable-next-line no-param-reassign
// eslint-disable-next-line no-return-assign
return keys.reduce((result, key) => (result += ` ${key}="${attributes[key]}"`), '');
};
const genHtmlTemplateFunc = (contentOrContentFn) => ({ attributes, files, meta, publicPath, title }) => {
const scripts = (files.js || [])
.map(({ fileName }) => `<script src="${publicPath}${fileName}"${makeHtmlAttributes(attributes.script)}></script>`)
.join('\n');
const links = (files.css || [])
.map(({ fileName }) => `<link href="${publicPath}${fileName}" rel="stylesheet"${makeHtmlAttributes(attributes.link)}>`)
.join('\n');
const metas = meta.map((input) => `<meta${makeHtmlAttributes(input)}>`).join('\n');
return `<!doctype html>
<html${makeHtmlAttributes(attributes.html)}>
<head>
${metas}
<title>${title}</title>
<style>
html,
body {
padding: 0;
margin: 0;
width: 100%;
height: 100%;
box-sizing: border-box;
}
body {
padding: 10px;
}
*::before,
*::after {
box-sizing: border-box;
}
* {
box-sizing: inherit;
}
#testResult {
display: none;
position: fixed;
top: 0;
right: 0;
padding: 5px;
background: white;
}
#testResult.passed {
display: block;
background: lime;
}
#testResult.passed::before {
content: 'passed';
}
#testResult.failed {
display: block;
background: red;
}
#testResult.failed::before {
content: 'failed';
}
</style>
${links}
</head>
<body>
${(typeof contentOrContentFn === 'function' ? contentOrContentFn() : contentOrContentFn) || ''}
${scripts}
<div id="testResult"></div>
</body>
</html>`;
};
const getAllFilesFrom = (dir, except) => {
const result = [];
fs.readdirSync(dir).forEach((dirOrFile) => {
if (!except.includes(dirOrFile)) {
const dirOrFileResolved = path.resolve(dir, dirOrFile);
if (fs.statSync(dirOrFileResolved).isDirectory()) {
result.push(...getAllFilesFrom(dirOrFileResolved));
}
result.push(dirOrFileResolved);
}
});
return result;
};
const createCacheObj = (testPath) => {
const testFileName = path.basename(testPath);
const testFiles = getAllFilesFrom(path.dirname(testPath), [deploymentConfig.build, testFileName]);
const obj = {};
testFiles.forEach((dir) => {
obj[dir] = crypto.createHash(cacheHash).update(fs.readFileSync(dir, cacheEncoding), cacheEncoding).digest('hex');
});
return obj;
};
const filesChanged = (testPath, cacheDir) => {
let result = true;
const cacheObjString = JSON.stringify(createCacheObj(testPath));
const getCacheFile = path.resolve(cacheDir, cacheFilePrefix + crypto.createHash(cacheHash).update(testPath, cacheEncoding).digest('hex'));
if (fs.existsSync(getCacheFile)) {
result = cacheObjString !== fs.readFileSync(getCacheFile, cacheEncoding);
}
if (result) {
fs.writeFileSync(getCacheFile, cacheObjString);
}
return result;
};
const setupRollupTest = async (rootDir, testPath, cacheDir, watch) => {
const rollupWatchers = [];
const rollupServers = [];
const testDir = path.dirname(testPath);
const testName = path.basename(testDir);
const changed = cacheDir && !watch ? filesChanged(testPath, cacheDir) : true;
const buildFolderExists = fs.existsSync(path.resolve(testDir, deploymentConfig.build));
if (changed || !buildFolderExists) {
const rollupConfigPath = path.resolve(rootDir, rollupConfigName);
if (fs.existsSync(rollupConfigPath)) {
const rollupConfig = require(rollupConfigPath); // eslint-disable-line
if (typeof rollupConfig === 'function') {
try {
const htmlFilePath = path.resolve(testDir, deploymentConfig.html.input);
const dist = path.resolve(testDir, deploymentConfig.build);
const getHtmlFileContent = () => (fs.existsSync(htmlFilePath) ? fs.readFileSync(htmlFilePath, 'utf8') : null);
const logBuilding = (re) => {
const text = re ? ' RE-BUILDING ' : ' BUILDING ';
console.log(`${chalk.bgBlue.bold.whiteBright(text)} ${chalk.blackBright(testPath)}`); // eslint-disable-line
};
const logBundleFinish = (duration) => {
if (duration) {
console.log(`Bundle finished after ${Math.round(duration / 1000)} seconds.`); // eslint-disable-line
} else {
console.log(`Bundle finished.`); // eslint-disable-line
}
};
let rollupConfigObj = rollupConfig(undefined, {
project: rootDir,
overwrite: ({ defaultConfig }) => {
return {
dist,
input: path.resolve(testDir, deploymentConfig.js.input),
file: deploymentConfig.js.output,
types: null,
minVersions: false,
esmBuild: false,
sourcemap: true,
name: testName,
pipeline: [
rollupPluginStyles(),
...defaultConfig.pipeline,
rollupPluginHtml({
title: `Jest-Browser: ${testName}`,
fileName: deploymentConfig.html.output,
template: genHtmlTemplateFunc(getHtmlFileContent),
meta: [{ charset: 'utf-8' }, { 'http-equiv': 'X-UA-Compatible', content: 'IE=edge' }],
}),
...(watch
? [
rollupAdditionalWatchFiles([htmlFilePath]),
rollupPluginServe({
contentBase: dist,
historyApiFallback: `/${deploymentConfig.html.output}`,
port: deploymentConfig.dev.servePort,
onListening(server) {
rollupServers.push(server);
},
}),
rollupPluginLivereload({
watch: dist,
port: deploymentConfig.dev.livereloadPort,
}),
]
: []),
],
};
},
silent: true,
fast: true,
});
if (!Array.isArray(rollupConfigObj)) {
rollupConfigObj = [rollupConfigObj];
}
for (let i = 0; i < rollupConfigObj.length; i++) {
const inputConfig = rollupConfigObj[i];
let { output } = inputConfig;
if (!Array.isArray(output)) {
output = [output];
}
if (watch) {
let firstWatch = true;
const rollupWatcher = rollup.watch({
...inputConfig,
output,
});
// eslint-disable-next-line no-await-in-loop
await new Promise((resolve) => {
rollupWatcher.on('event', ({ code, duration, error, result }) => {
if (code === 'ERROR') {
console.log('Error:', error); // eslint-disable-line
}
if (code === 'START') {
if (firstWatch) {
console.log(''); // eslint-disable-line
}
logBuilding(!firstWatch);
}
if (code === 'BUNDLE_END') {
logBundleFinish(duration);
if (result && result.close) {
result.close();
}
}
if (code === 'END') {
console.log('Watching for changes, press ENTER to continue.'); // eslint-disable-line
console.log(''); // eslint-disable-line
if (firstWatch) {
firstWatch = false;
resolve();
}
}
});
});
rollupWatchers.push(rollupWatcher);
} else {
console.log(''); // eslint-disable-line
logBuilding();
const startTime = Date.now();
// eslint-disable-next-line no-await-in-loop
const bundle = await rollup.rollup(inputConfig);
for (let v = 0; v < output.length; v++) {
const outputConfig = output[i];
// eslint-disable-next-line no-await-in-loop
await bundle.write(outputConfig);
const endTime = Date.now();
logBundleFinish(endTime - startTime);
}
console.log(''); // eslint-disable-line
}
}
if (watch) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
await new Promise((resolve) => {
rl.on('line', () => {
resolve();
});
rl.on('close', () => {
resolve();
});
});
rl.close();
rollupWatchers.forEach((watcher) => {
watcher.close();
});
rollupServers.forEach((server) => {
server.close();
});
if (rollupPluginLivereload && global.PLUGIN_LIVERELOAD && global.PLUGIN_LIVERELOAD.server) {
global.PLUGIN_LIVERELOAD.server.close();
global.PLUGIN_LIVERELOAD.server = null;
}
}
} catch (e) {
console.warn(e);
}
}
}
}
};
const cleanupRollupTest = async (testPath, cache) => {
if (!cache) {
await del(path.resolve(path.dirname(testPath), deploymentConfig.build));
}
};
module.exports = { setupRollupTest, cleanupRollupTest };
-2
View File
@@ -1,2 +0,0 @@
jest.setTimeout(60000 * 5);
context.setDefaultTimeout(60000 * 5);
-7
View File
@@ -1,7 +0,0 @@
const express = require('express');
const deploymentConfig = require('./jest-browser.rollup.config.js');
const app = express();
app.use(express.static(deploymentConfig.root));
app.listen(deploymentConfig.port);
-10
View File
@@ -1,10 +0,0 @@
const path = require('path');
const deploymentConfig = require('./jest-browser.rollup.config.js');
module.exports = {
process: (src, filePath) => {
const deploymentPath = path.relative(deploymentConfig.root, filePath);
const split = deploymentPath.split(path.sep);
return `module.exports = ${JSON.stringify(`http://127.0.0.1:${deploymentConfig.port}/${path.posix.join(...split)}`)}`;
},
};
+73
View File
@@ -0,0 +1,73 @@
const fs = require('fs');
const path = require('path');
const rollupPluginStyles = require('rollup-plugin-styles');
const rollupPluginServe = require('rollup-plugin-serve');
const rollupPluginLivereload = require('rollup-plugin-livereload');
const createRollupConfig = require('../rollup/rollup.config');
const rollupPluginHtml = require('./rollup.pluginHtml');
const rollupAdditionalWatchFiles = require('./rollup.pluginAdditionalWatchFiles');
const portRange = {
min: 20000,
max: 60000,
};
const meta = {
dist: './.build',
html: './index.html',
input: './index.browser',
};
module.exports = (testDir, onListening = null) => {
const name = path.basename(testDir);
const htmlFilePath = path.resolve(testDir, meta.html);
const dist = path.resolve(testDir, meta.dist);
const htmlName = `${name}.html`;
const { min, max } = portRange;
const port = Math.floor(Math.random() * (max - min + 1) + min);
return createRollupConfig({
project: name,
mode: 'dev',
paths: {
dist,
src: path.resolve(testDir, './'),
},
versions: {
minified: false,
module: false,
},
rollup: {
input: path.resolve(testDir, meta.input),
context: 'this',
moduleContext: () => 'this',
output: {
sourcemap: true,
},
plugins: [
rollupPluginStyles(),
rollupPluginHtml(`Playwright: ${name}`, htmlName, () =>
fs.existsSync(htmlFilePath) ? fs.readFileSync(htmlFilePath, 'utf8') : null
),
...(onListening
? [
rollupAdditionalWatchFiles([htmlFilePath]),
rollupPluginServe({
contentBase: dist,
historyApiFallback: `/${htmlName}`,
host: '127.0.0.1',
port,
onListening,
}),
rollupPluginLivereload({
watch: dist,
port: port - 1,
verbose: false,
}),
]
: []),
],
},
});
};
@@ -0,0 +1,13 @@
const fs = require('fs');
module.exports = (files) => ({
buildStart() {
if (files) {
files.forEach((file) => {
if (fs.existsSync(file)) {
this.addWatchFile(file);
}
});
}
},
});
+102
View File
@@ -0,0 +1,102 @@
const rollupPluginHtml = require('@rollup/plugin-html');
const makeHtmlAttributes = (attributes) => {
if (!attributes) {
return '';
}
const keys = Object.keys(attributes);
// eslint-disable-next-line no-param-reassign
// eslint-disable-next-line no-return-assign
return keys.reduce((result, key) => (result += ` ${key}="${attributes[key]}"`), '');
};
const genHtmlTemplateFunc = (contentOrContentFn) => ({
attributes,
files,
meta,
publicPath,
title,
}) => {
const scripts = (files.js || [])
.map(
({ fileName }) =>
`<script src="${publicPath}${fileName}"${makeHtmlAttributes(attributes.script)}></script>`
)
.join('\n');
const links = (files.css || [])
.map(
({ fileName }) =>
`<link href="${publicPath}${fileName}" rel="stylesheet"${makeHtmlAttributes(
attributes.link
)}>`
)
.join('\n');
const metas = meta.map((input) => `<meta${makeHtmlAttributes(input)}>`).join('\n');
return `<!doctype html>
<html${makeHtmlAttributes(attributes.html)}>
<head>
${metas}
<title>${title}</title>
<style>
html,
body {
padding: 0;
margin: 0;
width: 100%;
height: 100%;
box-sizing: border-box;
}
body {
padding: 10px;
}
*::before,
*::after {
box-sizing: border-box;
}
* {
box-sizing: inherit;
}
#testResult {
display: none;
position: fixed;
top: 0;
right: 0;
padding: 5px;
background: white;
}
#testResult.passed {
display: block;
background: lime;
}
#testResult.passed::before {
content: 'passed';
}
#testResult.failed {
display: block;
background: red;
}
#testResult.failed::before {
content: 'failed';
}
</style>
${links}
</head>
<body>
${(typeof contentOrContentFn === 'function' ? contentOrContentFn() : contentOrContentFn) || ''}
${scripts}
<div id="testResult"></div>
</body>
</html>`;
};
module.exports = (title, fileName, getHtmlContent) =>
rollupPluginHtml({
title,
fileName,
template: genHtmlTemplateFunc(getHtmlContent),
meta: [{ charset: 'utf-8' }, { 'http-equiv': 'X-UA-Compatible', content: 'IE=edge' }],
});
+14
View File
@@ -0,0 +1,14 @@
module.exports = {
presets: [
[
'@babel/preset-env',
{
loose: true,
bugfixes: true,
targets: {
esmodules: true,
},
},
],
],
};
+13
View File
@@ -0,0 +1,13 @@
module.exports = {
presets: [
[
'@babel/preset-env',
{
loose: true,
targets: {
ie: '11',
},
},
],
],
};
+21
View File
@@ -0,0 +1,21 @@
module.exports = {
project: null,
mode: 'build',
paths: {
src: './src',
dist: './dist',
types: './types',
},
versions: {
minified: true,
module: true,
},
alias: {},
rollup: {
input: './src/index',
output: {
sourcemap: true,
exports: 'auto',
},
},
};
+107
View File
@@ -0,0 +1,107 @@
const path = require('path');
const { babel: rollupBabelInputPlugin } = require('@rollup/plugin-babel');
const { terser: rollupTerser } = require('rollup-plugin-terser');
const rollupTs = require('rollup-plugin-ts');
const babelConfigUmd = require('./babel.config.umd');
const babelConfigEsm = require('./babel.config.esm');
const { rollupCommonjs, rollupResolve, rollupAlias } = require('./pipeline.common.plugins');
const { extensions } = require('../../resolve.config.json');
const createOutputWithMinifiedVersion = (output, esm, buildMinifiedVersion) =>
[output].concat(
buildMinifiedVersion
? [
{
...output,
compact: true,
file: output.file.replace('.js', '.min.js'),
sourcemap: false,
plugins: [
...(output.plugins || []),
rollupTerser({
ecma: esm ? 2015 : 5,
safari10: true,
mangle: {
safari10: true,
properties: {
regex: /^_/,
},
},
compress: {
evaluate: false,
},
}),
],
},
]
: []
);
module.exports = (esm, options, declarationFiles = false) => {
const { rollup, paths, versions, alias } = options;
const { output: rollupOutput, input, plugins = [], ...rollupOptions } = rollup;
const { name, file, globals, exports, sourcemap: rawSourcemap, ...outputConfig } = rollupOutput;
const { minified: buildMinifiedVersion } = versions;
const { src: srcPath, dist: distPath, types: typesPath } = paths;
const sourcemap = rawSourcemap;
const output = createOutputWithMinifiedVersion(
{
...outputConfig,
...(!esm && {
name,
globals,
exports,
}),
sourcemap,
format: esm ? 'esm' : 'umd',
generatedCode: esm ? 'es2015' : 'es5',
file: path.resolve(distPath, `${file}${esm ? '.esm' : ''}.js`),
},
esm,
buildMinifiedVersion
);
return {
input,
output,
...rollupOptions,
plugins: [
rollupAlias(alias),
rollupTs({
tsconfig: (resolvedConfig) => ({
...resolvedConfig,
declaration: declarationFiles,
declarationDir: typesPath,
}),
include: ['*.ts+(|x)', '**/*.ts+(|x)'],
exclude: ['node_modules', '**/node_modules/*'],
}),
rollupResolve(srcPath),
rollupCommonjs(sourcemap),
rollupBabelInputPlugin({
...(esm ? babelConfigEsm : babelConfigUmd),
assumptions: {
iterableIsArray: true,
noNewArrows: true,
noClassCalls: true,
ignoreToPrimitiveHint: true,
ignoreFunctionLength: true,
},
plugins: [
'@babel/plugin-transform-runtime',
['@babel/plugin-proposal-class-properties', { loose: true }],
['@babel/plugin-proposal-private-methods', { loose: true }],
],
babelHelpers: 'runtime',
shouldPrintComment: () => false,
caller: {
name: 'babel-rollup-build',
},
extensions,
}),
...plugins,
],
};
};
+23
View File
@@ -0,0 +1,23 @@
const { nodeResolve: rollupPluginResolve } = require('@rollup/plugin-node-resolve');
const rollupPluginCommonjs = require('@rollup/plugin-commonjs');
const rollupPluginAlias = require('@rollup/plugin-alias');
const { extensions, directories } = require('../../resolve.config.json');
module.exports = {
rollupAlias: (aliasEntries) =>
rollupPluginAlias({
entries: aliasEntries,
}),
rollupCommonjs: (sourcemap) =>
rollupPluginCommonjs({
sourceMap: sourcemap,
extensions,
}),
rollupResolve: (srcPath) =>
rollupPluginResolve({
mainFields: ['browser', 'umd:main', 'module', 'main'],
rootDir: srcPath,
moduleDirectories: directories,
extensions,
}),
};
+37
View File
@@ -0,0 +1,37 @@
const path = require('path');
const { default: rollupEsBuild } = require('rollup-plugin-esbuild');
const { rollupCommonjs, rollupResolve, rollupAlias } = require('./pipeline.common.plugins');
module.exports = (options) => {
const { rollup, paths, alias } = options;
const { output: rollupOutput, input, plugins = [], ...rollupOptions } = rollup;
const { file, sourcemap: rawSourcemap, ...outputConfig } = rollupOutput;
const { src: srcPath, dist: distPath } = paths;
const sourcemap = rawSourcemap;
const output = {
...outputConfig,
sourcemap: true,
format: 'esm',
generatedCode: 'es2015',
file: path.resolve(distPath, `${file}.js`),
};
return {
input,
output,
...rollupOptions,
plugins: [
rollupAlias(alias),
rollupResolve(srcPath),
rollupEsBuild({
include: /\.[jt]sx?$/,
sourceMap: true,
target: 'es6',
tsconfig: './tsconfig.json',
}),
rollupCommonjs(sourcemap),
...plugins,
],
};
};
+107
View File
@@ -0,0 +1,107 @@
const fs = require('fs');
const path = require('path');
const glob = require('glob');
const resolve = require('../../resolve.config.json');
const pkg = require('../../package.json');
const defaultOptions = require('./defaultOptions');
const pipelineBuild = require('./pipeline.build');
const pipelineDev = require('./pipeline.dev');
const repoRoot = path.resolve(__dirname, '../../');
const appendExtension = (file) =>
path.extname(file) === '' ? file + resolve.extensions.find((ext) => fs.existsSync(path.resolve(`${file}${ext}`))) : file;
const normalizePath = (pathName) => (pathName ? pathName.split(path.sep).join(path.posix.sep) : pathName);
const resolvePath = (basePath, pathToResolve, appendExt) => {
const result = pathToResolve ? (path.isAbsolute(pathToResolve) ? pathToResolve : path.resolve(basePath, pathToResolve)) : null;
return normalizePath(result && appendExt ? appendExtension(result) : result);
};
const getWorkspaceAliases = () =>
pkg.workspaces
.map((pattern) => glob.sync(pattern, { cwd: repoRoot }))
.flat()
.reduce((obj, resolvedPath) => {
let projTsConfig;
const absolutePath = path.resolve(repoRoot, resolvedPath);
try {
projTsConfig = require(`${path.resolve(repoRoot, resolvedPath)}/tsconfig.json`);
} catch {}
obj[`@/${path.basename(absolutePath)}`] = `${normalizePath(
path.resolve(absolutePath, projTsConfig?.compilerOptions?.baseUrl || defaultOptions.paths.src)
)}`;
return obj;
}, {});
const mergeAndResolveOptions = (userOptions) => {
const { mode: defaultMode, paths: defaultPaths, versions: defaultVersions, alias: defaultAlias, rollup: defaultRollup } = defaultOptions;
const { project, mode: rawMode, paths: rawPaths = {}, versions: rawVersions = {}, alias: rawAlias = {}, rollup: rawRollup = {} } = userOptions;
const projectPath = process.cwd();
const mergedOptions = {
project: project || path.basename(projectPath),
mode: rawMode || defaultMode,
repoRoot,
paths: {
...defaultPaths,
...rawPaths,
},
versions: {
...defaultVersions,
...rawVersions,
},
alias: {
...getWorkspaceAliases(),
...defaultAlias,
...rawAlias,
},
rollup: {
...defaultRollup,
...rawRollup,
output: {
...defaultRollup.output,
...(rawRollup.output || {}),
},
},
};
const { src, dist, types, tests } = mergedOptions.paths;
mergedOptions.paths.src = resolvePath(projectPath, src);
mergedOptions.paths.dist = resolvePath(projectPath, dist);
mergedOptions.paths.types = resolvePath(projectPath, types);
mergedOptions.paths.tests = resolvePath(projectPath, tests);
mergedOptions.rollup.input = resolvePath(projectPath, mergedOptions.rollup.input, true);
mergedOptions.rollup.output = {
...(mergedOptions.rollup.output || {}),
name: mergedOptions.rollup.output?.name || mergedOptions.project,
file: mergedOptions.rollup.output?.file || mergedOptions.project.toLocaleLowerCase(),
};
return mergedOptions;
};
const createConfig = (userOptions = {}) => {
const options = mergeAndResolveOptions(userOptions);
const { project, mode, versions } = options;
const { module: buildModuleVersion } = versions;
const isBuild = mode === 'build';
if (isBuild) {
console.log('');
console.log('PROJECT : ', project);
console.log('OPTIONS : ', options);
const umd = pipelineBuild(false, options, true);
const esm = buildModuleVersion ? pipelineBuild(true, options) : null;
return [umd, esm].filter((build) => !!build);
}
return pipelineDev(options);
};
module.exports = createConfig;
-18
View File
@@ -1,18 +0,0 @@
const path = require('path');
const deploymentConfig = path.resolve(__dirname, './config/jest-browser.rollup.config.js');
const testServerPath = path.resolve(__dirname, './config/jest-test-server.js');
module.exports = {
browsers: ['chromium', 'firefox', 'webkit'],
collectCoverage: true,
launchType: 'LAUNCH',
launchOptions: {
headless: false,
},
serverOptions: {
command: `node ${testServerPath}`,
port: deploymentConfig.port,
launchTimeout: 10000,
},
};
+5 -52
View File
@@ -1,64 +1,17 @@
const path = require('path');
const resolve = require('./resolve.config');
const browserRollupConfig = require('./config/jest-browser.rollup.config.js');
const testServerLoaderPath = path.resolve(__dirname, './config/jest-test-server.loader.js');
const jsdomSetupFile = path.resolve(__dirname, './config/jest-jsdom.setup.js');
const browserGlobalSetupPath = path.resolve(__dirname, './config/jest-browser.globalSetup.js');
const browserGlobalTeardownPath = path.resolve(__dirname, './config/jest-browser.globalTeardown.js');
const browserTestEnvironmentPath = path.resolve(__dirname, './config/jest-browser.env.js');
const browserSetupAfterEnvFile = path.resolve(__dirname, './config/jest-browser.setupAfterEnv.js');
// For a detailed explanation regarding each configuration property, visit:
// https://jestjs.io/docs/en/configuration.html
const base = {
module.exports = {
clearMocks: true,
coverageDirectory: './.coverage/jsdom',
coverageDirectory: './.coverage/jest',
testEnvironment: 'jsdom',
moduleDirectories: resolve.directories,
moduleFileExtensions: resolve.extensions.map((ext) => ext.replace(/\./, '')),
testPathIgnorePatterns: ['\\\\node_modules\\\\'],
};
const browserBase = {
...base,
collectCoverage: false,
preset: 'jest-playwright-preset',
globalSetup: browserGlobalSetupPath,
globalTeardown: browserGlobalTeardownPath,
testEnvironment: browserTestEnvironmentPath,
setupFilesAfterEnv: [browserSetupAfterEnvFile],
testMatch: ['**/tests/browser/**/*.test.[jt]s?(x)'],
coveragePathIgnorePatterns: ['/node_modules/', `/${browserRollupConfig.build}/`],
transform: {
'^.+\\.[jt]sx?$': 'babel-jest',
[`^.+${browserRollupConfig.build}.+${browserRollupConfig.html.output}?$`]: testServerLoaderPath,
},
};
module.exports = {
...base,
projects: [
{
...base,
displayName: 'jsdom',
setupFilesAfterEnv: [jsdomSetupFile],
testMatch: ['**/tests/jsdom/**/*.test.[jt]s?(x)'],
},
{
...browserBase,
displayName: {
name: 'browser',
color: 'white',
},
},
{
...browserBase,
displayName: {
name: 'browser-dev',
color: 'white',
},
},
],
displayName: 'jest',
setupFilesAfterEnv: [path.resolve(__dirname, './config/jest/jest.setup.js')],
testMatch: ['**/tests/jest/**/*.test.[jt]s?(x)'],
};
+25 -32
View File
@@ -4,66 +4,58 @@
"packages/*"
],
"devDependencies": {
"@babel/core": "^7.14.6",
"@babel/plugin-transform-runtime": "^7.14.5",
"@babel/preset-env": "^7.14.7",
"@babel/preset-typescript": "^7.14.5",
"@rollup/plugin-babel": "^5.3.0",
"@rollup/plugin-commonjs": "^17.0.0",
"@babel/core": "^7.18.2",
"@babel/plugin-transform-runtime": "^7.18.2",
"@babel/preset-env": "^7.18.2",
"@babel/preset-typescript": "^7.17.12",
"@babel/runtime": "^7.18.2",
"@playwright/test": "^1.22.2",
"@rollup/plugin-alias": "^3.1.9",
"@rollup/plugin-babel": "^5.3.1",
"@rollup/plugin-commonjs": "^22.0.0",
"@rollup/plugin-html": "^0.2.0",
"@rollup/plugin-inject": "^4.0.2",
"@rollup/plugin-node-resolve": "^11.0.1",
"@rollup/plugin-typescript": "^5.0.2",
"@rollup/plugin-node-resolve": "^13.3.0",
"@testing-library/dom": "^7.26.3",
"@types/jest": "^26.0.24",
"@typescript-eslint/eslint-plugin": "^3.7.0",
"@typescript-eslint/parser": "^3.7.0",
"babel-jest": "^27.0.6",
"babel-plugin-istanbul": "^6.0.0",
"babel-jest": "^28.1.1",
"bufferutil": "^4.0.1",
"canvas": "^2.6.1",
"chalk": "^4.1.0",
"core-js": "^3.6.5",
"del": "^5.1.0",
"esbuild": "^0.14.42",
"eslint": "^7.5.0",
"eslint-config-airbnb": "^18.2.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-import": "^2.22.0",
"eslint-plugin-jest": "^24.3.6",
"eslint-plugin-jest-playwright": "^0.4.1",
"eslint-plugin-json": "^2.1.2",
"eslint-plugin-jsx-a11y": "^6.3.1",
"eslint-plugin-prettier": "^3.1.4",
"eslint-plugin-react": "^7.20.3",
"eslint-plugin-react-hooks": "^4.0.8",
"expect-playwright": "^0.3.4",
"express": "^4.17.1",
"glob": "^7.1.6",
"jest": "^27.0.6",
"jest-circus": "^27.0.6",
"jest-dev-server": "^4.4.0",
"jest-environment-node": "^27.0.6",
"jest-playwright-preset": "^1.7.0",
"jest-runner": "^27.0.6",
"mkdirp": "^1.0.4",
"node-sass": "^4.14.1",
"playwright": "^1.12.3",
"playwright-chromium": "^1.12.3",
"playwright-core": "^1.12.3",
"playwright-firefox": "^1.12.3",
"playwright-webkit": "1.12.3",
"jest": "^28.1.1",
"node-sass": "^7.0.1",
"playwright": "^1.22.2",
"playwright-chromium": "^1.22.2",
"playwright-core": "^1.22.2",
"playwright-firefox": "^1.22.2",
"playwright-webkit": "^1.22.2",
"prettier": "^2.0.5",
"prettier-eslint": "^11.0.0",
"rollup": "^2.36.1",
"rollup": "^2.75.5",
"rollup-plugin-esbuild": "^4.9.1",
"rollup-plugin-livereload": "^2.0.0",
"rollup-plugin-prettier": "^2.1.0",
"rollup-plugin-serve": "^1.1.0",
"rollup-plugin-styles": "^3.10.0",
"rollup-plugin-terser": "^6.1.0",
"rollup-plugin-typescript2": "^0.27.1",
"rollup-plugin-ts": "^3.0.1",
"should": "^13.2.3",
"tslib": "^2.2.0",
"typescript": "^4.3.2",
"tslib": "^2.4.0",
"typescript": "^4.7.3",
"utf-8-validate": "^5.0.2"
},
"scripts": {
@@ -72,6 +64,7 @@
"test:browser": "yarn workspaces run test:browser",
"test:browser:quick": "yarn workspaces run test:browser:quick",
"test:browser-dev": "yarn workspaces run test:browser-dev",
"test:playwright": "yarn workspaces run test:playwright",
"build": "yarn workspaces run build",
"lint": "npx eslint --fix ."
}
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
@@ -0,0 +1,15 @@
<svg width="410" height="404" viewBox="0 0 410 404" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M399.641 59.5246L215.643 388.545C211.844 395.338 202.084 395.378 198.228 388.618L10.5817 59.5563C6.38087 52.1896 12.6802 43.2665 21.0281 44.7586L205.223 77.6824C206.398 77.8924 207.601 77.8904 208.776 77.6763L389.119 44.8058C397.439 43.2894 403.768 52.1434 399.641 59.5246Z" fill="url(#paint0_linear)"/>
<path d="M292.965 1.5744L156.801 28.2552C154.563 28.6937 152.906 30.5903 152.771 32.8664L144.395 174.33C144.198 177.662 147.258 180.248 150.51 179.498L188.42 170.749C191.967 169.931 195.172 173.055 194.443 176.622L183.18 231.775C182.422 235.487 185.907 238.661 189.532 237.56L212.947 230.446C216.577 229.344 220.065 232.527 219.297 236.242L201.398 322.875C200.278 328.294 207.486 331.249 210.492 326.603L212.5 323.5L323.454 102.072C325.312 98.3645 322.108 94.137 318.036 94.9228L279.014 102.454C275.347 103.161 272.227 99.746 273.262 96.1583L298.731 7.86689C299.767 4.27314 296.636 0.855181 292.965 1.5744Z" fill="url(#paint1_linear)"/>
<defs>
<linearGradient id="paint0_linear" x1="6.00017" y1="32.9999" x2="235" y2="344" gradientUnits="userSpaceOnUse">
<stop stop-color="#41D1FF"/>
<stop offset="1" stop-color="#BD34FE"/>
</linearGradient>
<linearGradient id="paint1_linear" x1="194.651" y1="8.81818" x2="236.076" y2="292.989" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFEA83"/>
<stop offset="0.0833333" stop-color="#FFDD35"/>
<stop offset="1" stop-color="#FFA800"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>OverlayScrollbars example</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
@@ -0,0 +1,17 @@
{
"name": "overlayscrollbars-example",
"private": true,
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"devDependencies": {
"typescript": "^4.5.4",
"vite": "^2.9.9"
},
"dependencies": {
"overlayscrollbars": "file:./../overlayscrollbars"
}
}
@@ -0,0 +1,8 @@
import './style.css';
const app = document.querySelector<HTMLDivElement>('#app')!;
app.innerHTML = `
<h1>Hello Vite!</h1>
<a href="https://vitejs.dev/guide/features.html" target="_blank">Documentation</a>
`;
@@ -0,0 +1,8 @@
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ESNext", "DOM"],
"moduleResolution": "Node",
"strict": true,
"sourceMap": true,
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"noEmit": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"skipLibCheck": true
},
"include": ["src"]
}
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+8 -2
View File
@@ -3,12 +3,18 @@
"private": true,
"description": "OverlayScrollbars version 2",
"version": "0.0.1",
"files": [
"src",
"dist"
],
"types": "types/index.d.ts",
"scripts": {
"test": "jest --coverage --runInBand --detectOpenHandles",
"test:jsdom": "jest --coverage --runInBand --detectOpenHandles --selectProjects jsdom --testPathPattern",
"test:jsdom": "jest --coverage --runInBand --detectOpenHandles --testPathPattern",
"test:browser": "jest --runInBand --detectOpenHandles --selectProjects browser --testPathPattern",
"test:browser:quick": "jest --runInBand --detectOpenHandles --selectProjects browser --testPathIgnorePatterns=\"/node_modules/|/structureLifecycle/\"",
"test:browser-dev": "jest --runInBand --detectOpenHandles --selectProjects browser-dev --testPathPattern",
"build": "rollup -c"
"build": "rollup -c",
"test:playwright": "playwright test"
}
}
@@ -0,0 +1 @@
module.exports = require('../../playwright.config.base');
@@ -0,0 +1 @@
module.exports = require('../../playwright.rollup.base');
+10 -9
View File
@@ -1,11 +1,12 @@
const base = require('../../rollup.config.base');
const createRollupConfig = require('../../rollup.config.base');
const { devDependencies, peerDependencies } = require('./package.json');
const config = {
name: 'OverlayScrollbars',
exports: 'auto',
globals: {
jquery: 'jQuery',
module.exports = createRollupConfig({
project: 'OverlayScrollbars',
rollup: {
external: Object.keys(devDependencies || {}).concat(Object.keys(peerDependencies || {})),
output: {
exports: 'auto',
},
},
};
module.exports = (_, ...args) => base(config, ...args);
});
@@ -1,4 +1,16 @@
import { XY, WH, TRBL, CacheValues, PartialOptions, each, hasOwnProperty, isNumber, scrollLeft, scrollTop, assignDeep } from 'support';
import {
XY,
WH,
TRBL,
CacheValues,
PartialOptions,
each,
hasOwnProperty,
isNumber,
scrollLeft,
scrollTop,
assignDeep,
} from 'support';
import { OSOptions } from 'options';
import { getEnvironment } from 'environment';
import { StructureSetup } from 'setups/structureSetup';
@@ -17,10 +29,7 @@ export type Lifecycle = (
force: boolean
) => Partial<LifecycleAdaptiveUpdateHints> | void;
export interface LifecycleOptionInfo<T> {
readonly _value: T;
_changed: boolean;
}
export type LifecycleOptionInfo<T> = [T, boolean];
export interface LifecycleCommunication {
_paddingInfo: {
@@ -64,13 +73,11 @@ export interface LifecycleHub {
}
const getPropByPath = <T>(obj: any, path: string): T =>
obj ? path.split('.').reduce((o, prop) => (o && hasOwnProperty(o, prop) ? o[prop] : undefined), obj) : undefined;
obj
? path.split('.').reduce((o, prop) => (o && hasOwnProperty(o, prop) ? o[prop] : undefined), obj)
: undefined;
const booleanCacheValuesFallback: CacheValues<boolean> = {
_value: false,
_previous: false,
_changed: false,
};
const booleanCacheValuesFallback: CacheValues<boolean> = [false, false, false];
const lifecycleCommunicationFallback: LifecycleCommunication = {
_paddingInfo: {
_absolute: false,
@@ -100,7 +107,11 @@ const lifecycleCommunicationFallback: LifecycleCommunication = {
},
};
export const createLifecycleHub = (options: OSOptions, structureSetup: StructureSetup, scrollbarsSetup: ScrollbarsSetup): LifecycleHubInstance => {
export const createLifecycleHub = (
options: OSOptions,
structureSetup: StructureSetup,
scrollbarsSetup: ScrollbarsSetup
): LifecycleHubInstance => {
let lifecycleCommunication = lifecycleCommunicationFallback;
const { _viewport } = structureSetup._targetObj;
const {
@@ -110,7 +121,8 @@ export const createLifecycleHub = (options: OSOptions, structureSetup: Structure
_addListener: addEnvironmentListener,
_removeListener: removeEnvironmentListener,
} = getEnvironment();
const doViewportArrange = !_nativeScrollbarStyling && (_nativeScrollbarIsOverlaid.x || _nativeScrollbarIsOverlaid.y);
const doViewportArrange =
!_nativeScrollbarStyling && (_nativeScrollbarIsOverlaid.x || _nativeScrollbarIsOverlaid.y);
const instance: LifecycleHub = {
_options: options,
_structureSetup: structureSetup,
@@ -120,11 +132,21 @@ export const createLifecycleHub = (options: OSOptions, structureSetup: Structure
lifecycleCommunication = assignDeep({}, lifecycleCommunication, newLifecycleCommunication);
},
};
const lifecycles: Lifecycle[] = [createTrinsicLifecycle(instance), createPaddingLifecycle(instance), createOverflowLifecycle(instance)];
const lifecycles: Lifecycle[] = [
createTrinsicLifecycle(instance),
createPaddingLifecycle(instance),
createOverflowLifecycle(instance),
];
const updateLifecycles = (updateHints?: Partial<LifecycleUpdateHints> | null, changedOptions?: Partial<OSOptions> | null, force?: boolean) => {
const updateLifecycles = (
updateHints?: Partial<LifecycleUpdateHints> | null,
changedOptions?: Partial<OSOptions> | null,
force?: boolean
) => {
let {
// eslint-disable-next-line prefer-const
_directionIsRTL,
// eslint-disable-next-line prefer-const
_heightIntrinsic,
_sizeChanged = force || false,
_hostMutation = force || false,
@@ -133,13 +155,19 @@ export const createLifecycleHub = (options: OSOptions, structureSetup: Structure
} = updateHints || {};
const finalDirectionIsRTL =
_directionIsRTL || (_sizeObserver ? _sizeObserver._getCurrentCacheValues(force)._directionIsRTL : booleanCacheValuesFallback);
_directionIsRTL ||
(_sizeObserver
? _sizeObserver._getCurrentCacheValues(force)._directionIsRTL
: booleanCacheValuesFallback);
const finalHeightIntrinsic =
_heightIntrinsic || (_trinsicObserver ? _trinsicObserver._getCurrentCacheValues(force)._heightIntrinsic : booleanCacheValuesFallback);
const checkOption: LifecycleCheckOption = (path) => ({
_value: getPropByPath(options, path),
_changed: force || getPropByPath(changedOptions, path) !== undefined,
});
_heightIntrinsic ||
(_trinsicObserver
? _trinsicObserver._getCurrentCacheValues(force)._heightIntrinsic
: booleanCacheValuesFallback);
const checkOption: LifecycleCheckOption = (path) => [
getPropByPath(options, path),
force || getPropByPath(changedOptions, path) !== undefined,
];
const adjustScrollOffset = doViewportArrange || !_flexboxGlue;
const scrollOffsetX = adjustScrollOffset && scrollLeft(_viewport);
const scrollOffsetY = adjustScrollOffset && scrollTop(_viewport);
@@ -186,9 +214,15 @@ export const createLifecycleHub = (options: OSOptions, structureSetup: Structure
options.callbacks.onUpdated();
}
};
const { _sizeObserver, _trinsicObserver, _updateObserverOptions, _destroy: destroyObservers } = lifecycleHubOservers(instance, updateLifecycles);
const {
_sizeObserver,
_trinsicObserver,
_updateObserverOptions,
_destroy: destroyObservers,
} = lifecycleHubOservers(instance, updateLifecycles);
const update = (changedOptions?: Partial<OSOptions> | null, force?: boolean) => updateLifecycles(null, changedOptions, force);
const update = (changedOptions?: Partial<OSOptions> | null, force?: boolean) =>
updateLifecycles(null, changedOptions, force);
const envUpdateListener = update.bind(null, null, true);
addEnvironmentListener(envUpdateListener);
@@ -1,24 +1,40 @@
import { CacheValues, diffClass, debounce, isArray, isNumber, each, indexOf, isString, attr, removeAttr } from 'support';
import {
CacheValues,
diffClass,
debounce,
isArray,
isNumber,
each,
indexOf,
isString,
attr,
removeAttr,
} from 'support';
import { getEnvironment } from 'environment';
import { createSizeObserver, SizeObserverCallbackParams } from 'observers/sizeObserver';
import { createTrinsicObserver } from 'observers/trinsicObserver';
import { createDOMObserver, DOMObserver } from 'observers/domObserver';
import { LifecycleHub, LifecycleCheckOption, LifecycleUpdateHints } from 'lifecycles/lifecycleHub';
//const hostSelector = `.${classNameHost}`;
// const hostSelector = `.${classNameHost}`;
// TODO: observer textarea attrs if textarea
// TODO: test _ignoreContentChange & _ignoreNestedTargetChange for content dom observer
// TODO: test _ignoreTargetChange for target dom observer
//const viewportSelector = `.${classNameViewport}`;
//const contentSelector = `.${classNameContent}`;
// const viewportSelector = `.${classNameViewport}`;
// const contentSelector = `.${classNameContent}`;
const ignorePrefix = 'os-';
const viewportAttrsFromTarget = ['tabindex'];
const baseStyleChangingAttrsTextarea = ['wrap', 'cols', 'rows'];
const baseStyleChangingAttrs = ['id', 'class', 'style', 'open'];
const ignoreTargetChange = (target: Node, attrName: string, oldValue: string | null, newValue: string | null) => {
const ignoreTargetChange = (
target: Node,
attrName: string,
oldValue: string | null,
newValue: string | null
) => {
if (attrName === 'class' && oldValue && newValue) {
const diff = diffClass(oldValue, newValue);
return !!diff.find((addedOrRemovedClass) => addedOrRemovedClass.indexOf(ignorePrefix) !== 0);
@@ -26,7 +42,10 @@ const ignoreTargetChange = (target: Node, attrName: string, oldValue: string | n
return false;
};
export const lifecycleHubOservers = (instance: LifecycleHub, updateLifecycles: (updateHints?: Partial<LifecycleUpdateHints> | null) => unknown) => {
export const lifecycleHubOservers = (
instance: LifecycleHub,
updateLifecycles: (updateHints?: Partial<LifecycleUpdateHints> | null) => unknown
) => {
let debounceTimeout: number | false | undefined;
let debounceMaxDelay: number | false | undefined;
let contentMutationObserver: DOMObserver | undefined;
@@ -35,24 +54,37 @@ export const lifecycleHubOservers = (instance: LifecycleHub, updateLifecycles: (
const { _host, _viewport, _content } = _targetObj;
const { _isTextarea } = _targetCtx;
const { _nativeScrollbarStyling, _flexboxGlue } = getEnvironment();
const contentMutationObserverAttr = _isTextarea ? baseStyleChangingAttrsTextarea : baseStyleChangingAttrs.concat(baseStyleChangingAttrsTextarea);
const updateLifecyclesWithDebouncedAdaptiveUpdateHints = debounce(updateLifecycles as (updateHints: Partial<LifecycleUpdateHints>) => any, {
_timeout: () => debounceTimeout,
_maxDelay: () => debounceMaxDelay,
_mergeParams(prev, curr) {
const { _sizeChanged: prevSizeChanged, _hostMutation: prevHostMutation, _contentMutation: prevContentMutation } = prev[0];
const { _sizeChanged: currSizeChanged, _hostMutation: currvHostMutation, _contentMutation: currContentMutation } = curr[0];
const merged: [Partial<LifecycleUpdateHints>] = [
{
_sizeChanged: prevSizeChanged || currSizeChanged,
_hostMutation: prevHostMutation || currvHostMutation,
_contentMutation: prevContentMutation || currContentMutation,
},
];
const contentMutationObserverAttr = _isTextarea
? baseStyleChangingAttrsTextarea
: baseStyleChangingAttrs.concat(baseStyleChangingAttrsTextarea);
const updateLifecyclesWithDebouncedAdaptiveUpdateHints = debounce(
updateLifecycles as (updateHints: Partial<LifecycleUpdateHints>) => any,
{
_timeout: () => debounceTimeout,
_maxDelay: () => debounceMaxDelay,
_mergeParams(prev, curr) {
const {
_sizeChanged: prevSizeChanged,
_hostMutation: prevHostMutation,
_contentMutation: prevContentMutation,
} = prev[0];
const {
_sizeChanged: currSizeChanged,
_hostMutation: currvHostMutation,
_contentMutation: currContentMutation,
} = curr[0];
const merged: [Partial<LifecycleUpdateHints>] = [
{
_sizeChanged: prevSizeChanged || currSizeChanged,
_hostMutation: prevHostMutation || currvHostMutation,
_contentMutation: prevContentMutation || currContentMutation,
},
];
return merged;
},
});
return merged;
},
}
);
const updateViewportAttrsFromHost = (attributes?: string[]) => {
each(attributes || viewportAttrsFromTarget, (attribute) => {
@@ -71,8 +103,15 @@ export const lifecycleHubOservers = (instance: LifecycleHub, updateLifecycles: (
_heightIntrinsic: heightIntrinsic,
});
};
const onSizeChanged = ({ _sizeChanged, _directionIsRTLCache, _appear }: SizeObserverCallbackParams) => {
const updateFn = !_sizeChanged || _appear ? updateLifecycles : updateLifecyclesWithDebouncedAdaptiveUpdateHints;
const onSizeChanged = ({
_sizeChanged,
_directionIsRTLCache,
_appear,
}: SizeObserverCallbackParams) => {
const updateFn =
!_sizeChanged || _appear
? updateLifecycles
: updateLifecyclesWithDebouncedAdaptiveUpdateHints;
updateFn({
_sizeChanged,
_directionIsRTL: _directionIsRTLCache,
@@ -80,7 +119,9 @@ export const lifecycleHubOservers = (instance: LifecycleHub, updateLifecycles: (
};
const onContentMutation = (contentChangedTroughEvent: boolean) => {
// if contentChangedTroughEvent is true its already debounced
const updateFn = contentChangedTroughEvent ? updateLifecycles : updateLifecyclesWithDebouncedAdaptiveUpdateHints;
const updateFn = contentChangedTroughEvent
? updateLifecycles
: updateLifecyclesWithDebouncedAdaptiveUpdateHints;
updateFn({
_contentMutation: true,
});
@@ -95,8 +136,12 @@ export const lifecycleHubOservers = (instance: LifecycleHub, updateLifecycles: (
}
};
const trinsicObserver = (_content || !_flexboxGlue) && createTrinsicObserver(_host, onTrinsicChanged);
const sizeObserver = createSizeObserver(_host, onSizeChanged, { _appear: true, _direction: !_nativeScrollbarStyling });
const trinsicObserver =
(_content || !_flexboxGlue) && createTrinsicObserver(_host, onTrinsicChanged);
const sizeObserver = createSizeObserver(_host, onSizeChanged, {
_appear: true,
_direction: !_nativeScrollbarStyling,
});
const hostMutationObserver = createDOMObserver(_host, false, onHostMutation, {
_styleChangingAttributes: baseStyleChangingAttrs,
_attributes: baseStyleChangingAttrs.concat(viewportAttrsFromTarget),
@@ -104,9 +149,13 @@ export const lifecycleHubOservers = (instance: LifecycleHub, updateLifecycles: (
});
const updateOptions = (checkOption: LifecycleCheckOption) => {
const { _value: elementEvents, _changed: elementEventsChanged } = checkOption<Array<[string, string]> | null>('updating.elementEvents');
const { _value: attributes, _changed: attributesChanged } = checkOption<string[] | null>('updating.attributes');
const { _value: debounce, _changed: debounceChanged } = checkOption<Array<number> | number | null>('updating.debounce');
const [elementEvents, elementEventsChanged] = checkOption<Array<[string, string]> | null>(
'updating.elementEvents'
);
const [attributes, attributesChanged] = checkOption<string[] | null>('updating.attributes');
const [debounceValue, debounceChanged] = checkOption<Array<number> | number | null>(
'updating.debounce'
);
const updateContentMutationObserver = elementEventsChanged || attributesChanged;
if (updateContentMutationObserver) {
@@ -119,7 +168,7 @@ export const lifecycleHubOservers = (instance: LifecycleHub, updateLifecycles: (
_attributes: contentMutationObserverAttr.concat(attributes || []),
_eventContentChange: elementEvents,
_ignoreNestedTargetChange: ignoreTargetChange,
//_nestedTargetSelector: hostSelector,
// _nestedTargetSelector: hostSelector,
/*
_ignoreContentChange: (mutation, isNestedTarget) => {
const { target, attributeName } = mutation;
@@ -135,13 +184,13 @@ export const lifecycleHubOservers = (instance: LifecycleHub, updateLifecycles: (
if (debounceChanged) {
updateLifecyclesWithDebouncedAdaptiveUpdateHints._flush();
if (isArray(debounce)) {
const timeout = debounce[0];
const maxWait = debounce[1];
if (isArray(debounceValue)) {
const timeout = debounceValue[0];
const maxWait = debounceValue[1];
debounceTimeout = isNumber(timeout) ? timeout : false;
debounceMaxDelay = isNumber(maxWait) ? maxWait : false;
} else if (isNumber(debounce)) {
debounceTimeout = debounce;
} else if (isNumber(debounceValue)) {
debounceTimeout = debounceValue;
debounceMaxDelay = false;
} else {
debounceTimeout = false;
@@ -59,7 +59,12 @@ const sizeFraction = (elm: HTMLElement): WH<number> => {
};
};
const fractionalPixelRatioTollerance = () => (window.devicePixelRatio % 1 === 0 ? 0 : 1);
const setAxisOverflowStyle = (horizontal: boolean, overflowAmount: number, behavior: OverflowBehavior, styleObj: StyleObject) => {
const setAxisOverflowStyle = (
horizontal: boolean,
overflowAmount: number,
behavior: OverflowBehavior,
styleObj: StyleObject
) => {
const overflowKey: keyof StyleObject = horizontal ? 'overflowX' : 'overflowY';
const behaviorIsVisible = behavior.indexOf('visible') === 0;
const behaviorIsVisibleHidden = behavior === 'visible-hidden';
@@ -85,20 +90,43 @@ const setAxisOverflowStyle = (horizontal: boolean, overflowAmount: number, behav
* @returns
*/
export const createOverflowLifecycle = (lifecycleHub: LifecycleHub): Lifecycle => {
const { _structureSetup, _doViewportArrange, _getLifecycleCommunication, _setLifecycleCommunication } = lifecycleHub;
const {
_structureSetup,
_doViewportArrange,
_getLifecycleCommunication,
_setLifecycleCommunication,
} = lifecycleHub;
const { _host, _viewport, _viewportArrange } = _structureSetup._targetObj;
const { _update: updateViewportSizeFraction, _current: getCurrentViewportSizeFraction } = createCache<WH<number>>(
const [updateViewportSizeFraction, getCurrentViewportSizeFraction] = createCache<WH<number>>(
sizeFraction.bind(0, _viewport),
whCacheOptions
);
const { _update: updateViewportScrollSizeCache, _current: getCurrentViewportScrollSizeCache } = createCache<WH<number>>(
scrollSize.bind(0, _viewport),
whCacheOptions
);
const { _update: updateOverflowAmountCache, _current: getCurrentOverflowAmountCache } = createCache<WH<number>, OverflowAmountCacheContext>(
const [updateViewportScrollSizeCache, getCurrentViewportScrollSizeCache] = createCache<
WH<number>
>(scrollSize.bind(0, _viewport), whCacheOptions);
const [updateOverflowAmountCache, getCurrentOverflowAmountCache] = createCache<
WH<number>,
OverflowAmountCacheContext
>(
({ _viewportScrollSize, _viewportClientSize, _viewportSizeFraction }) => ({
w: max(0, round(max(0, _viewportScrollSize.w - _viewportClientSize.w) - (fractionalPixelRatioTollerance() || max(0, _viewportSizeFraction.w)))),
h: max(0, round(max(0, _viewportScrollSize.h - _viewportClientSize.h) - (fractionalPixelRatioTollerance() || max(0, _viewportSizeFraction.h)))),
w: max(
0,
round(
max(0, _viewportScrollSize.w - _viewportClientSize.w) -
(fractionalPixelRatioTollerance() || max(0, _viewportSizeFraction.w))
)
),
h: max(
0,
round(
max(0, _viewportScrollSize.h - _viewportClientSize.h) -
(fractionalPixelRatioTollerance() || max(0, _viewportSizeFraction.h))
)
),
}),
whCacheOptions
);
@@ -108,22 +136,35 @@ export const createOverflowLifecycle = (lifecycleHub: LifecycleHub): Lifecycle =
* @param viewportOverflowState The current overflow state.
* @param heightIntrinsic Whether the host height is intrinsic or not.
*/
const fixFlexboxGlue = (viewportOverflowState: ViewportOverflowState, heightIntrinsic: boolean) => {
const fixFlexboxGlue = (
viewportOverflowState: ViewportOverflowState,
heightIntrinsic: boolean
) => {
style(_viewport, {
height: '',
});
if (heightIntrinsic) {
const { _absolute: paddingAbsolute, _padding: padding } = _getLifecycleCommunication()._paddingInfo;
const { _nativeScrollbarIsOverlaid } = getEnvironment();
const {
_absolute: paddingAbsolute,
_padding: padding,
} = _getLifecycleCommunication()._paddingInfo;
const { _overflowScroll, _scrollbarsHideOffset } = viewportOverflowState;
const hostSizeFraction = sizeFraction(_host);
const hostClientSize = clientSize(_host);
// padding subtraction is only needed if padding is absolute or if viewport is content-box
const paddingVertical = paddingAbsolute || style(_viewport, 'boxSizing') === 'content-box' ? padding.b + padding.t : 0;
const fractionalClientHeight = hostClientSize.h + (abs(hostSizeFraction.h) < 1 ? hostSizeFraction.h : 0);
const isContentBox = style(_viewport, 'boxSizing') === 'content-box';
const paddingVertical = paddingAbsolute || isContentBox ? padding.b + padding.t : 0;
const fractionalClientHeight =
hostClientSize.h + (abs(hostSizeFraction.h) < 1 ? hostSizeFraction.h : 0);
const subtractXScrollbar = !(_nativeScrollbarIsOverlaid.x && isContentBox);
style(_viewport, {
height: fractionalClientHeight + (_overflowScroll.x ? _scrollbarsHideOffset.x : 0) - paddingVertical,
height:
fractionalClientHeight +
(_overflowScroll.x && subtractXScrollbar ? _scrollbarsHideOffset.x : 0) -
paddingVertical,
});
}
};
@@ -134,19 +175,39 @@ export const createOverflowLifecycle = (lifecycleHub: LifecycleHub): Lifecycle =
* @param viewportStyleObj The viewport style object where the overflow scroll property can be read of, or undefined if shall be determined.
* @returns A object which contains informations about the current overflow state.
*/
const getViewportOverflowState = (showNativeOverlaidScrollbars: boolean, viewportStyleObj?: StyleObject): ViewportOverflowState => {
const { _nativeScrollbarSize, _nativeScrollbarIsOverlaid, _nativeScrollbarStyling } = getEnvironment();
const getViewportOverflowState = (
showNativeOverlaidScrollbars: boolean,
viewportStyleObj?: StyleObject
): ViewportOverflowState => {
const {
_nativeScrollbarSize,
_nativeScrollbarIsOverlaid,
_nativeScrollbarStyling,
} = getEnvironment();
const { x: overlaidX, y: overlaidY } = _nativeScrollbarIsOverlaid;
const determineOverflow = !viewportStyleObj;
const arrangeHideOffset = !_nativeScrollbarStyling && !showNativeOverlaidScrollbars ? overlaidScrollbarsHideOffset : 0;
const styleObj = determineOverflow ? style(_viewport, ['overflowX', 'overflowY']) : viewportStyleObj;
const arrangeHideOffset =
!_nativeScrollbarStyling && !showNativeOverlaidScrollbars ? overlaidScrollbarsHideOffset : 0;
const styleObj = determineOverflow
? style(_viewport, ['overflowX', 'overflowY'])
: viewportStyleObj;
const scroll = {
x: styleObj!.overflowX === 'scroll',
y: styleObj!.overflowY === 'scroll',
};
const scrollbarsHideOffset = {
x: scroll.x && !_nativeScrollbarStyling ? (overlaidX ? arrangeHideOffset : _nativeScrollbarSize.x) : 0,
y: scroll.y && !_nativeScrollbarStyling ? (overlaidY ? arrangeHideOffset : _nativeScrollbarSize.y) : 0,
x:
scroll.x && !_nativeScrollbarStyling
? overlaidX
? arrangeHideOffset
: _nativeScrollbarSize.x
: 0,
y:
scroll.y && !_nativeScrollbarStyling
? overlaidY
? arrangeHideOffset
: _nativeScrollbarSize.y
: 0,
};
return {
@@ -173,8 +234,18 @@ export const createOverflowLifecycle = (lifecycleHub: LifecycleHub): Lifecycle =
overflow: OverflowOption,
viewportStyleObj: StyleObject
): ViewportOverflowState => {
const { _visible: xVisible, _behavior: xVisibleBehavior } = setAxisOverflowStyle(true, overflowAmount!.w, overflow.x, viewportStyleObj);
const { _visible: yVisible, _behavior: yVisibleBehavior } = setAxisOverflowStyle(false, overflowAmount!.h, overflow.y, viewportStyleObj);
const { _visible: xVisible, _behavior: xVisibleBehavior } = setAxisOverflowStyle(
true,
overflowAmount!.w,
overflow.x,
viewportStyleObj
);
const { _visible: yVisible, _behavior: yVisibleBehavior } = setAxisOverflowStyle(
false,
overflowAmount!.h,
overflow.y,
viewportStyleObj
);
if (xVisible && !yVisible) {
viewportStyleObj.overflowX = xVisibleBehavior;
@@ -204,14 +275,26 @@ export const createOverflowLifecycle = (lifecycleHub: LifecycleHub): Lifecycle =
const { x: arrangeX, y: arrangeY } = _scrollbarsHideOffsetArrange;
const { x: hideOffsetX, y: hideOffsetY } = _scrollbarsHideOffset;
const { _viewportPaddingStyle: viewportPaddingStyle } = _getLifecycleCommunication();
const viewportArrangeHorizontalPaddingKey: keyof StyleObject = directionIsRTL ? 'paddingRight' : 'paddingLeft';
const viewportArrangeHorizontalPaddingValue = viewportPaddingStyle[viewportArrangeHorizontalPaddingKey] as number;
const viewportArrangeHorizontalPaddingKey: keyof StyleObject = directionIsRTL
? 'paddingRight'
: 'paddingLeft';
const viewportArrangeHorizontalPaddingValue = viewportPaddingStyle[
viewportArrangeHorizontalPaddingKey
] as number;
const viewportArrangeVerticalPaddingValue = viewportPaddingStyle.paddingTop as number;
const fractionalContentWidth = viewportScrollSize.w + (abs(viewportSizeFraction.w) < 1 ? viewportSizeFraction.w : 0);
const fractionalContenHeight = viewportScrollSize.h + (abs(viewportSizeFraction.h) < 1 ? viewportSizeFraction.h : 0);
const fractionalContentWidth =
viewportScrollSize.w + (abs(viewportSizeFraction.w) < 1 ? viewportSizeFraction.w : 0);
const fractionalContenHeight =
viewportScrollSize.h + (abs(viewportSizeFraction.h) < 1 ? viewportSizeFraction.h : 0);
const arrangeSize = {
w: hideOffsetY && arrangeY ? `${hideOffsetY + fractionalContentWidth - viewportArrangeHorizontalPaddingValue}px` : '',
h: hideOffsetX && arrangeX ? `${hideOffsetX + fractionalContenHeight - viewportArrangeVerticalPaddingValue}px` : '',
w:
hideOffsetY && arrangeY
? `${hideOffsetY + fractionalContentWidth - viewportArrangeHorizontalPaddingValue}px`
: '',
h:
hideOffsetX && arrangeX
? `${hideOffsetX + fractionalContenHeight - viewportArrangeVerticalPaddingValue}px`
: '',
};
// adjust content arrange / before element
@@ -221,7 +304,10 @@ export const createOverflowLifecycle = (lifecycleHub: LifecycleHub): Lifecycle =
const { cssRules } = sheet;
if (cssRules) {
if (!cssRules.length) {
sheet.insertRule(`#${attr(_viewportArrange, 'id')} + .${classNameViewportArrange}::before {}`, 0);
sheet.insertRule(
`#${attr(_viewportArrange, 'id')} + .${classNameViewportArrange}::before {}`,
0
);
}
// @ts-ignore
@@ -260,7 +346,9 @@ export const createOverflowLifecycle = (lifecycleHub: LifecycleHub): Lifecycle =
const { x: hideOffsetX, y: hideOffsetY } = _scrollbarsHideOffset;
const { _viewportPaddingStyle: viewportPaddingStyle } = _getLifecycleCommunication();
const horizontalMarginKey: keyof StyleObject = directionIsRTL ? 'marginLeft' : 'marginRight';
const viewportHorizontalPaddingKey: keyof StyleObject = directionIsRTL ? 'paddingLeft' : 'paddingRight';
const viewportHorizontalPaddingKey: keyof StyleObject = directionIsRTL
? 'paddingLeft'
: 'paddingRight';
const horizontalMarginValue = viewportPaddingStyle[horizontalMarginKey] as number;
const verticalMarginValue = viewportPaddingStyle.marginBottom as number;
const horizontalPaddingValue = viewportPaddingStyle[viewportHorizontalPaddingKey] as number;
@@ -275,7 +363,8 @@ export const createOverflowLifecycle = (lifecycleHub: LifecycleHub): Lifecycle =
// viewport arrange additional styles
if (viewportArrange) {
viewportStyleObj[viewportHorizontalPaddingKey] = horizontalPaddingValue + (arrangeY ? hideOffsetY : 0);
viewportStyleObj[viewportHorizontalPaddingKey] =
horizontalPaddingValue + (arrangeY ? hideOffsetY : 0);
viewportStyleObj.paddingBottom = verticalPaddingValue + (arrangeX ? hideOffsetX : 0);
}
};
@@ -293,7 +382,8 @@ export const createOverflowLifecycle = (lifecycleHub: LifecycleHub): Lifecycle =
viewportOverflowState?: ViewportOverflowState
): UndoViewportArrangeResult => {
if (_doViewportArrange) {
const finalViewportOverflowState = viewportOverflowState || getViewportOverflowState(showNativeOverlaidScrollbars);
const finalViewportOverflowState =
viewportOverflowState || getViewportOverflowState(showNativeOverlaidScrollbars);
const { _viewportPaddingStyle: viewportPaddingStyle } = _getLifecycleCommunication();
const { _flexboxGlue } = getEnvironment();
const { _scrollbarsHideOffsetArrange } = finalViewportOverflowState;
@@ -322,7 +412,12 @@ export const createOverflowLifecycle = (lifecycleHub: LifecycleHub): Lifecycle =
return {
_redoViewportArrange: () => {
hideNativeScrollbars(finalViewportOverflowState, directionIsRTL, _doViewportArrange, prevStyle);
hideNativeScrollbars(
finalViewportOverflowState,
directionIsRTL,
_doViewportArrange,
prevStyle
);
style(_viewport, prevStyle);
addClass(_viewport, classNameViewportArrange);
},
@@ -335,16 +430,32 @@ export const createOverflowLifecycle = (lifecycleHub: LifecycleHub): Lifecycle =
};
return (updateHints, checkOption, force) => {
const { _directionIsRTL, _heightIntrinsic, _sizeChanged, _hostMutation, _contentMutation, _paddingStyleChanged } = updateHints;
const {
_directionIsRTL,
_heightIntrinsic,
_sizeChanged,
_hostMutation,
_contentMutation,
_paddingStyleChanged,
} = updateHints;
const { _flexboxGlue, _nativeScrollbarStyling, _nativeScrollbarIsOverlaid } = getEnvironment();
const { _value: heightIntrinsic, _changed: heightIntrinsicChanged } = _heightIntrinsic;
const { _value: directionIsRTL, _changed: directionChanged } = _directionIsRTL;
const { _value: showNativeOverlaidScrollbarsOption, _changed: showNativeOverlaidScrollbarsChanged } = checkOption<boolean>(
'nativeScrollbarsOverlaid.show'
);
const showNativeOverlaidScrollbars = showNativeOverlaidScrollbarsOption && _nativeScrollbarIsOverlaid.x && _nativeScrollbarIsOverlaid.y;
const [heightIntrinsic, heightIntrinsicChanged] = _heightIntrinsic;
const [directionIsRTL, directionChanged] = _directionIsRTL;
const [showNativeOverlaidScrollbarsOption, showNativeOverlaidScrollbarsChanged] = checkOption<
boolean
>('nativeScrollbarsOverlaid.show');
const showNativeOverlaidScrollbars =
showNativeOverlaidScrollbarsOption &&
_nativeScrollbarIsOverlaid.x &&
_nativeScrollbarIsOverlaid.y;
const adjustFlexboxGlue =
!_flexboxGlue && (_sizeChanged || _contentMutation || _hostMutation || showNativeOverlaidScrollbarsChanged || heightIntrinsicChanged);
!_flexboxGlue &&
(_sizeChanged ||
_contentMutation ||
_hostMutation ||
showNativeOverlaidScrollbarsChanged ||
heightIntrinsicChanged);
let viewportSizeFractionCache: CacheValues<WH<number>> = getCurrentViewportSizeFraction(force);
let viewportScrollSizeCache: CacheValues<WH<number>> = getCurrentViewportScrollSizeCache(force);
let overflowAmuntCache: CacheValues<WH<number>> = getCurrentOverflowAmountCache(force);
@@ -363,14 +474,29 @@ export const createOverflowLifecycle = (lifecycleHub: LifecycleHub): Lifecycle =
fixFlexboxGlue(preMeasureViewportOverflowState, !!heightIntrinsic);
}
if (_sizeChanged || _paddingStyleChanged || _contentMutation || showNativeOverlaidScrollbarsChanged || directionChanged) {
const { _redoViewportArrange, _viewportOverflowState: undoViewportArrangeOverflowState } = undoViewportArrange(
if (
_sizeChanged ||
_paddingStyleChanged ||
_contentMutation ||
showNativeOverlaidScrollbarsChanged ||
directionChanged
) {
const {
_redoViewportArrange,
_viewportOverflowState: undoViewportArrangeOverflowState,
} = undoViewportArrange(
showNativeOverlaidScrollbars,
directionIsRTL!,
preMeasureViewportOverflowState
);
const { _value: viewportSizeFraction, _changed: viewportSizeFractionCahnged } = (viewportSizeFractionCache = updateViewportSizeFraction(force));
const { _value: viewportScrollSize, _changed: viewportScrollSizeChanged } = (viewportScrollSizeCache = updateViewportScrollSizeCache(force));
const [
viewportSizeFraction,
viewportSizeFractionCahnged,
] = (viewportSizeFractionCache = updateViewportSizeFraction(force));
const [
viewportScrollSize,
viewportScrollSizeChanged,
] = (viewportScrollSizeCache = updateViewportScrollSizeCache(force));
const viewportContentSize = clientSize(_viewport);
let arrangedViewportScrollSize = viewportScrollSize!;
let arrangedViewportClientSize = viewportContentSize;
@@ -379,10 +505,17 @@ export const createOverflowLifecycle = (lifecycleHub: LifecycleHub): Lifecycle =
// if re measure is required (only required if content arrange strategy is used)
if (
(viewportScrollSizeChanged || viewportSizeFractionCahnged || showNativeOverlaidScrollbarsChanged) &&
(viewportScrollSizeChanged ||
viewportSizeFractionCahnged ||
showNativeOverlaidScrollbarsChanged) &&
undoViewportArrangeOverflowState &&
!showNativeOverlaidScrollbars &&
arrangeViewport(undoViewportArrangeOverflowState, viewportScrollSize!, viewportSizeFraction!, directionIsRTL!)
arrangeViewport(
undoViewportArrangeOverflowState,
viewportScrollSize!,
viewportSizeFraction!,
directionIsRTL!
)
) {
arrangedViewportClientSize = clientSize(_viewport);
arrangedViewportScrollSize = scrollSize(_viewport);
@@ -401,10 +534,10 @@ export const createOverflowLifecycle = (lifecycleHub: LifecycleHub): Lifecycle =
});
}
const { _value: viewportSizeFraction, _changed: viewportSizeFractionChanged } = viewportSizeFractionCache;
const { _value: viewportScrollSize, _changed: viewportScrollSizeChanged } = viewportScrollSizeCache;
const { _value: overflowAmount, _changed: overflowAmountChanged } = overflowAmuntCache;
const { _value: overflow, _changed: overflowChanged } = checkOption<OverflowOption>('overflow');
const [viewportSizeFraction, viewportSizeFractionChanged] = viewportSizeFractionCache;
const [viewportScrollSize, viewportScrollSizeChanged] = viewportScrollSizeCache;
const [overflowAmount, overflowAmountChanged] = overflowAmuntCache;
const [overflow, overflowChanged] = checkOption<OverflowOption>('overflow');
if (
_paddingStyleChanged ||
@@ -425,8 +558,18 @@ export const createOverflowLifecycle = (lifecycleHub: LifecycleHub): Lifecycle =
overflowX: '',
};
const viewportOverflowState = setViewportOverflowState(showNativeOverlaidScrollbars, overflowAmount!, overflow, viewportStyle);
const viewportArranged = arrangeViewport(viewportOverflowState, viewportScrollSize!, viewportSizeFraction!, directionIsRTL!);
const viewportOverflowState = setViewportOverflowState(
showNativeOverlaidScrollbars,
overflowAmount!,
overflow,
viewportStyle
);
const viewportArranged = arrangeViewport(
viewportOverflowState,
viewportScrollSize!,
viewportSizeFraction!,
directionIsRTL!
);
hideNativeScrollbars(viewportOverflowState, directionIsRTL!, viewportArranged, viewportStyle);
if (adjustFlexboxGlue) {
@@ -11,21 +11,24 @@ import { getEnvironment } from 'environment';
export const createPaddingLifecycle = (lifecycleHub: LifecycleHub): Lifecycle => {
const { _structureSetup, _setLifecycleCommunication } = lifecycleHub;
const { _host, _padding, _viewport } = _structureSetup._targetObj;
const { _update: updatePaddingCache, _current: currentPaddingCache } = createCache<TRBL>(topRightBottomLeft.bind(0, _host, 'padding'), {
_equal: equalTRBL,
_initialValue: topRightBottomLeft(),
});
const [updatePaddingCache, currentPaddingCache] = createCache<TRBL>(
topRightBottomLeft.bind(0, _host, 'padding'),
{
_equal: equalTRBL,
_initialValue: topRightBottomLeft(),
}
);
return (updateHints, checkOption, force) => {
let { _value: padding, _changed: paddingChanged } = currentPaddingCache(force);
let [padding, paddingChanged] = currentPaddingCache(force);
const { _nativeScrollbarStyling, _flexboxGlue } = getEnvironment();
const { _sizeChanged, _directionIsRTL, _contentMutation } = updateHints;
const { _value: directionIsRTL, _changed: directionChanged } = _directionIsRTL;
const { _value: paddingAbsolute, _changed: paddingAbsoluteChanged } = checkOption('paddingAbsolute');
const [directionIsRTL, directionChanged] = _directionIsRTL;
const [paddingAbsolute, paddingAbsoluteChanged] = checkOption('paddingAbsolute');
const contentMutation = !_flexboxGlue && _contentMutation;
if (_sizeChanged || paddingChanged || contentMutation) {
({ _value: padding, _changed: paddingChanged } = updatePaddingCache(force));
[padding, paddingChanged] = updatePaddingCache(force);
}
const paddingStyleChanged = paddingAbsoluteChanged || directionChanged || paddingChanged;
@@ -12,7 +12,7 @@ export const createTrinsicLifecycle = (lifecycleHub: LifecycleHub): Lifecycle =>
return (updateHints) => {
const { _heightIntrinsic } = updateHints;
const { _value: heightIntrinsic, _changed: heightIntrinsicChanged } = _heightIntrinsic;
const [heightIntrinsic, heightIntrinsicChanged] = _heightIntrinsic;
if (heightIntrinsicChanged) {
style(_content, {
@@ -22,6 +22,7 @@ import {
isArray,
isBoolean,
removeClass,
isObject,
} from 'support';
import { getEnvironment } from 'environment';
import {
@@ -53,24 +54,10 @@ export interface SizeObserver {
};
}
/*
const directionIsRTLMap = {
direction: ['rtl'],
'writing-mode': ['sideways-rl', 'tb', 'tb-rl', 'vertical-rl'],
};
const directionIsRTL = (elm: HTMLElement): boolean => {
let isRTL = false;
const styles = style(elm, ['direction', 'writing-mode']);
each(styles, (value, key) => {
isRTL = isRTL || indexOf(directionIsRTLMap[key], value) > -1;
});
return isRTL;
};
*/
const animationStartEventName = 'animationstart';
const scrollEventName = 'scroll';
const scrollAmount = 3333333;
const directionIsRTL = (elm: HTMLElement): boolean => style(elm, 'direction') === 'rtl';
const getElmDirectionIsRTL = (elm: HTMLElement): boolean => style(elm, 'direction') === 'rtl';
const domRectHasDimensions = (rect?: DOMRectReadOnly) => rect && (rect.height || rect.width);
/**
@@ -85,12 +72,20 @@ export const createSizeObserver = (
onSizeChangedCallback: (params: SizeObserverCallbackParams) => any,
options?: SizeObserverOptions
): SizeObserver => {
const { _direction: observeDirectionChange = false, _appear: observeAppearChange = false } = options || {};
const { _direction: observeDirectionChange = false, _appear: observeAppearChange = false } =
options || {};
const { _rtlScrollBehavior: rtlScrollBehavior } = getEnvironment();
const baseElements = createDOM(`<div class="${classNameSizeObserver}"><div class="${classNameSizeObserverListener}"></div></div>`);
const baseElements = createDOM(
`<div class="${classNameSizeObserver}"><div class="${classNameSizeObserverListener}"></div></div>`
);
const sizeObserver = baseElements[0] as HTMLElement;
const listenerElement = sizeObserver.firstChild as HTMLElement;
const { _update: updateResizeObserverContentRectCache } = createCache<DOMRectReadOnly, DOMRectReadOnly>(0, {
const getIsDirectionRTL = getElmDirectionIsRTL.bind(0, sizeObserver);
const [updateResizeObserverContentRectCache] = createCache<
DOMRectReadOnly | undefined,
DOMRectReadOnly
>(0, {
_initialValue: undefined,
_alwaysUpdateValues: true,
_equal: (currVal, newVal) =>
!(
@@ -99,26 +94,37 @@ export const createSizeObserver = (
(!domRectHasDimensions(currVal) && domRectHasDimensions(newVal))
),
});
const onSizeChangedCallbackProxy = (sizeChangedContext?: CacheValues<boolean> | ResizeObserverEntry[] | Event | boolean) => {
const hasDirectionCache = sizeChangedContext && isBoolean((sizeChangedContext as CacheValues<boolean>)._value);
const onSizeChangedCallbackProxy = (
sizeChangedContext?: CacheValues<boolean> | ResizeObserverEntry[] | Event | boolean
) => {
const isResizeObserverCall =
isArray(sizeChangedContext) &&
sizeChangedContext.length > 0 &&
isObject(sizeChangedContext[0]);
const hasDirectionCache =
!isResizeObserverCall && isBoolean((sizeChangedContext as CacheValues<boolean>)[0]);
let skip = false;
let appear: boolean | number | undefined = false;
let doDirectionScroll = true; // always true if sizeChangedContext is Event (appear callback or RO. Polyfill)
// if triggered from RO.
if (isArray(sizeChangedContext) && sizeChangedContext.length > 0) {
const { _previous, _value } = updateResizeObserverContentRectCache(0, sizeChangedContext.pop()!.contentRect);
const hasDimensions = domRectHasDimensions(_value);
const hadDimensions = domRectHasDimensions(_previous);
skip = !_previous || !hasDimensions; // skip on initial RO. call or if display is none
if (isResizeObserverCall) {
const [currRContentRect, , prevContentRect] = updateResizeObserverContentRectCache(
0,
(sizeChangedContext as ResizeObserverEntry[]).pop()!.contentRect
);
const hasDimensions = domRectHasDimensions(currRContentRect);
const hadDimensions = domRectHasDimensions(prevContentRect);
skip = !prevContentRect || !hasDimensions; // skip on initial RO. call or if display is none
appear = !hadDimensions && hasDimensions;
doDirectionScroll = !skip; // direction scroll when not skipping
}
// else if its triggered with DirectionCache
else if (hasDirectionCache) {
doDirectionScroll = (sizeChangedContext as CacheValues<boolean>)._changed; // direction scroll when DirectionCache changed, false otherwise
[, doDirectionScroll] = sizeChangedContext as CacheValues<boolean>; // direction scroll when DirectionCache changed, false otherwise
}
// else if it triggered with appear from polyfill
else {
@@ -126,21 +132,36 @@ export const createSizeObserver = (
}
if (observeDirectionChange && doDirectionScroll) {
const rtl = hasDirectionCache ? (sizeChangedContext as CacheValues<boolean>)._value : directionIsRTL(sizeObserver);
scrollLeft(sizeObserver, rtl ? (rtlScrollBehavior.n ? -scrollAmount : rtlScrollBehavior.i ? 0 : scrollAmount) : scrollAmount);
const rtl = hasDirectionCache
? (sizeChangedContext as CacheValues<boolean>)[0]
: getElmDirectionIsRTL(sizeObserver);
scrollLeft(
sizeObserver,
rtl
? rtlScrollBehavior.n
? -scrollAmount
: rtlScrollBehavior.i
? 0
: scrollAmount
: scrollAmount
);
scrollTop(sizeObserver, scrollAmount);
}
if (!skip) {
onSizeChangedCallback({
_sizeChanged: !hasDirectionCache,
_directionIsRTLCache: hasDirectionCache ? (sizeChangedContext as CacheValues<boolean>) : undefined,
_directionIsRTLCache: hasDirectionCache
? (sizeChangedContext as CacheValues<boolean>)
: undefined,
_appear: !!appear,
});
}
};
const offListeners: (() => void)[] = [];
let appearCallback: ((...args: any) => any) | false = observeAppearChange ? onSizeChangedCallbackProxy : false;
let appearCallback: ((...args: any) => any) | false = observeAppearChange
? onSizeChangedCallbackProxy
: false;
let directionIsRTLCache: Cache<boolean> | undefined;
if (ResizeObserverConstructor) {
@@ -196,7 +217,10 @@ export const createSizeObserver = (
reset();
};
push(offListeners, [on(expandElement, scrollEventName, onScroll), on(shrinkElement, scrollEventName, onScroll)]);
push(offListeners, [
on(expandElement, scrollEventName, onScroll),
on(shrinkElement, scrollEventName, onScroll),
]);
// lets assume that the divs will never be that large and a constant value is enough
style(expandElementChild, {
@@ -210,17 +234,20 @@ export const createSizeObserver = (
}
if (observeDirectionChange) {
directionIsRTLCache = createCache(directionIsRTL.bind(0, sizeObserver));
const { _update: updateDirectionIsRTLCache } = directionIsRTLCache;
directionIsRTLCache = createCache(getIsDirectionRTL, {
_initialValue: !getIsDirectionRTL(), // invert current value to trigger initial change
});
const [updateDirectionIsRTLCache] = directionIsRTLCache;
push(
offListeners,
on(sizeObserver, scrollEventName, (event: Event) => {
const directionIsRTLCacheValues = updateDirectionIsRTLCache();
console.log;
const { _value, _changed } = directionIsRTLCacheValues;
if (_changed) {
const [directionIsRTL, directionIsRTLChanged] = directionIsRTLCacheValues;
if (directionIsRTLChanged) {
removeClass(listenerElement, 'ltr rtl');
if (_value) {
if (directionIsRTL) {
addClass(listenerElement, 'rtl');
} else {
addClass(listenerElement, 'ltr');
@@ -255,12 +282,8 @@ export const createSizeObserver = (
_getCurrentCacheValues(force?: boolean) {
return {
_directionIsRTL: directionIsRTLCache
? directionIsRTLCache._current(force)
: {
_value: false,
_previous: false,
_changed: false,
},
? directionIsRTLCache[1](force) // get current cache values
: [false, false, false],
};
},
};
@@ -34,7 +34,7 @@ export const createTrinsicObserver = (
): TrinsicObserver => {
const trinsicObserver = createDiv(classNameTrinsicObserver);
const offListeners: (() => void)[] = [];
const { _update: updateHeightIntrinsicCache, _current: getCurrentHeightIntrinsicCache } = createCache<
const [updateHeightIntrinsicCache, getCurrentHeightIntrinsicCache] = createCache<
boolean,
IntersectionObserverEntry | WH<number>
>(
@@ -47,18 +47,24 @@ export const createTrinsicObserver = (
}
);
const triggerOnTrinsicChangedCallback = (
updateValue?: IntersectionObserverEntry | WH<number>
) => {
if (updateValue) {
const heightIntrinsic = updateHeightIntrinsicCache(0, updateValue);
const [, heightIntrinsicChanged] = heightIntrinsic;
if (heightIntrinsicChanged) {
onTrinsicChangedCallback(heightIntrinsic);
}
}
};
if (IntersectionObserverConstructor) {
const intersectionObserverInstance: IntersectionObserver = new IntersectionObserverConstructor(
(entries: IntersectionObserverEntry[]) => {
if (entries && entries.length > 0) {
const last = entries.pop();
if (last) {
const heightIntrinsic = updateHeightIntrinsicCache(0, last);
if (heightIntrinsic._changed) {
onTrinsicChangedCallback(heightIntrinsic);
}
}
triggerOnTrinsicChangedCallback(entries.pop());
}
},
{ root: target }
@@ -70,10 +76,7 @@ export const createTrinsicObserver = (
} else {
const onSizeChanged = () => {
const newSize = offsetSize(trinsicObserver);
const heightIntrinsicCache = updateHeightIntrinsicCache(0, newSize);
if (heightIntrinsicCache._changed) {
onTrinsicChangedCallback(heightIntrinsicCache);
}
triggerOnTrinsicChangedCallback(newSize);
};
push(offListeners, createSizeObserver(trinsicObserver, onSizeChanged)._destroy);
onSizeChanged();
@@ -7,7 +7,11 @@ import { OSOptions, optionsTemplate } from 'options';
import { getEnvironment } from 'environment';
export interface OverlayScrollbarsStatic {
(target: OSTarget | OSInitializationObject, options?: PartialOptions<OSOptions>, extensions?: any): OverlayScrollbars;
(
target: OSTarget | OSInitializationObject,
options?: PartialOptions<OSOptions>,
extensions?: any
): OverlayScrollbars;
}
export interface OverlayScrollbars {
@@ -29,7 +33,8 @@ export const OverlayScrollbars: OverlayScrollbarsStatic = (
const currentOptions: OSOptions = assignDeep(
{},
_getDefaultOptions(),
validateOptions(options || ({} as PartialOptions<OSOptions>), optionsTemplate, null, true)._validated
validateOptions(options || ({} as PartialOptions<OSOptions>), optionsTemplate, null, true)
._validated
);
const structureSetup: StructureSetup = createStructureSetup(target);
const scrollbarsSetup: ScrollbarsSetup = createScrollbarsSetup(target, structureSetup);
@@ -38,7 +43,12 @@ export const OverlayScrollbars: OverlayScrollbarsStatic = (
const instance: OverlayScrollbars = {
options(newOptions?: PartialOptions<OSOptions>) {
if (newOptions) {
const { _validated: _changedOptions } = validateOptions(newOptions, optionsTemplate, currentOptions, true);
const { _validated: _changedOptions } = validateOptions(
newOptions,
optionsTemplate,
currentOptions,
true
);
if (!isEmptyObject(_changedOptions)) {
assignDeep(currentOptions, _changedOptions);
+34 -35
View File
@@ -1,41 +1,44 @@
export interface CacheValues<T> {
readonly _value?: T;
readonly _previous?: T;
_changed: boolean;
}
export type CacheValues<T> = [
T, // value
boolean, // changed
T | undefined // previous
];
export type Cache<Value, Ctx = undefined> = [
CacheUpdate<Value, Ctx>,
(force?: boolean) => CacheValues<Value> // getCurrent
];
export interface CacheOptions<T> {
// initial value of _value.
_initialValue: T;
// Custom comparison function if shallow compare isn't enough. Returns true if nothing changed.
_equal?: EqualCachePropFunction<T>;
// Initial value for _value
_initialValue?: T;
// If true updates always _value and _previous, otherwise they update only when changed
// If true always updates _value and _previous, otherwise they update only when they changed.
_alwaysUpdateValues?: boolean;
}
export interface Cache<T, C = undefined> {
_current: (force?: boolean) => CacheValues<T>;
_update: CacheUpdate<T, C>;
}
export type CacheUpdate<T, C> = undefined extends C
? (force?: boolean | 0, context?: C) => CacheValues<T>
: (force: boolean | 0, context: C) => CacheValues<T>;
export type UpdateCachePropFunction<T, C> = undefined extends C
? (context?: C, current?: T, previous?: T) => T
: C extends T
? ((context: C, current?: T, previous?: T) => T) | 0
: (context: C, current?: T, previous?: T) => T;
export type UpdateCachePropFunction<Value, Ctx> = undefined extends Ctx
? (context?: Ctx, current?: Value, previous?: Value) => Value
: Ctx extends Value
? ((context: Ctx, current?: Value, previous?: Value) => Value) | 0
: (context: Ctx, current?: Value, previous?: Value) => Value;
export type EqualCachePropFunction<T> = (currentVal?: T, newVal?: T) => boolean;
export const createCache = <T, C = undefined>(update: UpdateCachePropFunction<T, C>, options?: CacheOptions<T>): Cache<T, C> => {
const { _equal, _initialValue, _alwaysUpdateValues } = options || {};
let _value: T | undefined = _initialValue;
let _previous: T | undefined;
export const createCache = <Value, Ctx = undefined>(
update: UpdateCachePropFunction<Value, Ctx>,
options: CacheOptions<Value>
): Cache<Value, Ctx> => {
const { _initialValue, _equal, _alwaysUpdateValues } = options || {};
let _value: Value = _initialValue;
let _previous: Value | undefined;
const cacheUpdate = ((force?: boolean | 0, context?: C) => {
const cacheUpdate = ((force?: boolean | 0, context?: Ctx) => {
const curr = _value;
// @ts-ignore
// update can only not be a function if C extends T as described in "UpdateCachePropFunction" type definition
@@ -48,19 +51,15 @@ export const createCache = <T, C = undefined>(update: UpdateCachePropFunction<T,
_previous = curr;
}
return {
_value,
_previous,
_changed: changed,
};
}) as CacheUpdate<T, C>;
return [_value, changed, _previous];
}) as CacheUpdate<Value, Ctx>;
return {
_update: cacheUpdate,
_current: (force?: boolean) => ({
return [
cacheUpdate,
(force?: boolean) => [
_value,
!!force, // changed
_previous,
_changed: !!force,
}),
};
],
];
};
@@ -1,55 +0,0 @@
import 'jest-playwright-preset';
import 'expect-playwright';
import url from './.build/build.html';
describe('StructureLifecycle', () => {
beforeEach(async () => {
await jestPlaywright.resetPage();
await page.goto(url);
});
[false, true].forEach(async (nativeScrollbarStyling) => {
const withText = nativeScrollbarStyling ? 'with' : 'without';
const nss = async () => {
if (!nativeScrollbarStyling) {
await page.click('#nss');
await page.waitForTimeout(500);
}
};
describe(`structureLifecycles ${withText} native scrollbar styling`, () => {
test('default', async () => {
await nss();
await page.click('#start');
await expect(page).toHaveSelector('#testResult.passed');
});
test('without flexbox glue & css custom props', async () => {
await nss();
await page.click('#fbg');
await page.waitForTimeout(500);
await page.click('#ccp');
await page.waitForTimeout(500);
await page.click('#start');
await expect(page).toHaveSelector('#testResult.passed');
});
// firefox can't simulate partially overlaid scrollbars, boost speed by omitting webkit
test.jestPlaywrightSkip({ browsers: ['firefox', 'webkit'] }, 'with partially overlaid scrollbars', async () => {
await nss();
await page.click('#po');
await page.waitForTimeout(500);
await page.click('#start');
await expect(page).toHaveSelector('#testResult.passed');
});
test('with fully overlaid scrollbars', async () => {
await nss();
await page.click('#fo');
await page.waitForTimeout(500);
await page.click('#start');
await expect(page).toHaveSelector('#testResult.passed');
});
});
});
});
@@ -1,15 +0,0 @@
import 'jest-playwright-preset';
import 'expect-playwright';
import url from './.build/build.html';
describe('DOMObserver', () => {
beforeEach(async () => {
await jestPlaywright.resetPage();
await page.goto(url);
});
test('test', async () => {
await page.click('#start');
await expect(page).toHaveSelector('#testResult.passed');
});
});
@@ -1,22 +0,0 @@
import 'jest-playwright-preset';
import 'expect-playwright';
import url from './.build/build.html';
describe('SizeObserver', () => {
beforeEach(async () => {
await jestPlaywright.resetPage();
await page.goto(url);
});
test('with ResizeOserver', async () => {
await page.click('#start');
await expect(page).toHaveSelector('#testResult.passed');
});
test('with ResizeOserver polyfill', async () => {
await page.click('#roPolyfill');
await page.waitForTimeout(500);
await page.click('#start');
await expect(page).toHaveSelector('#testResult.passed');
});
});
@@ -1,31 +0,0 @@
import 'jest-playwright-preset';
import 'expect-playwright';
import url from './.build/build.html';
describe('TrinsicObserver', () => {
beforeEach(async () => {
await jestPlaywright.resetPage();
await page.goto(url);
});
test('with IntersectionObserver', async () => {
await page.click('#start');
await expect(page).toHaveSelector('#testResult.passed');
});
test('with ResizeObserver', async () => {
await page.click('#ioPolyfill');
await page.waitForTimeout(500);
await page.click('#start');
await expect(page).toHaveSelector('#testResult.passed');
});
test('with ResizeObserver polyfill', async () => {
await page.click('#ioPolyfill');
await page.waitForTimeout(500);
await page.click('#roPolyfill');
await page.waitForTimeout(500);
await page.click('#start');
await expect(page).toHaveSelector('#testResult.passed');
});
});
@@ -0,0 +1,902 @@
import {
Environment,
StructureInitializationStaticElement,
StructureInitializationDynamicElement,
} from 'environment';
import { OSTarget, StructureInitialization } from 'typings';
import { createStructureSetup, StructureSetup } from 'setups/structureSetup';
import { isHTMLElement } from 'support';
const mockGetEnvironment = jest.fn();
jest.mock('environment', () => {
return {
getEnvironment: jest.fn().mockImplementation(() => mockGetEnvironment()),
};
});
interface StructureSetupProxy {
input: OSTarget | StructureInitialization;
setup: StructureSetup;
}
const textareaId = 'textarea';
const textareaHostId = 'host';
const elementId = 'target';
const dynamicContent = 'text<p>paragraph</p>';
const textareaContent = `<textarea id="${textareaId}">text</textarea>`;
const getSnapshot = () => document.body.innerHTML;
const getTarget = (textarea?: boolean) =>
document.getElementById(textarea ? textareaId : elementId)!;
const fillBody = (textarea?: boolean, customDOM?: (content: string, hostId: string) => string) => {
document.body.innerHTML = `
<nav></nav>
${
customDOM
? customDOM(
textarea ? textareaContent : dynamicContent,
textarea ? textareaHostId : elementId
)
: textarea
? textareaContent
: `<div id="${elementId}">${dynamicContent}</div>`
}
<footer></footer>
`;
return getSnapshot();
};
const clearBody = () => {
document.body.innerHTML = '';
};
const getElements = (textarea?: boolean) => {
const target = getTarget(textarea);
const host = document.querySelector('.os-host')!;
const padding = document.querySelector('.os-padding')!;
const viewport = document.querySelector('.os-viewport')!;
const content = document.querySelector('.os-content')!;
return {
target,
host,
padding,
viewport,
content,
};
};
const assertCorrectDOMStructure = (textarea?: boolean) => {
const { target, host, padding, viewport, content } = getElements(textarea);
expect(host).toBeTruthy();
expect(viewport).toBeTruthy();
expect(viewport.parentElement).toBe(padding || host);
if (content) {
expect(content.parentElement).toBe(viewport);
}
if (padding) {
expect(padding.parentElement).toBe(host);
}
expect(host.parentElement).toBe(document.body);
expect(host.previousElementSibling).toBe(document.querySelector('nav'));
expect(host.nextElementSibling).toBe(document.querySelector('footer'));
const contentElm = content || viewport;
if (textarea) {
expect(target.parentElement).toBe(contentElm);
expect(contentElm.innerHTML).toBe(textareaContent);
} else {
expect(target).toBe(host);
expect(contentElm.innerHTML).toBe(dynamicContent);
}
};
const createStructureSetupProxy = (
target: OSTarget | StructureInitialization
): StructureSetupProxy => ({
input: target,
setup: createStructureSetup(target),
});
const assertCorrectSetup = (
textarea: boolean,
setupProxy: StructureSetupProxy,
environment: Environment
): StructureSetup => {
const { input, setup } = setupProxy;
const { _targetObj, _targetCtx, _destroy } = setup;
const { _target, _host, _padding, _viewport, _content } = _targetObj;
const { target, host, padding, viewport, content } = getElements(textarea);
const isTextarea = target.matches('textarea');
const isBody = target.matches('body');
expect(textarea).toBe(isTextarea);
expect(_target).toBe(target);
expect(_host).toBe(host);
if (padding || _padding) {
expect(_padding).toBe(padding);
} else {
expect(padding).toBeFalsy();
expect(_padding).toBeFalsy();
}
if (viewport || _viewport) {
expect(_viewport).toBe(viewport);
} else {
expect(viewport).toBeFalsy();
expect(_viewport).toBeFalsy();
}
if (content || _content) {
expect(_content).toBe(content);
} else {
expect(content).toBeFalsy();
expect(_content).toBeFalsy();
}
const { _isTextarea, _isBody, _bodyElm, _htmlElm, _documentElm, _windowElm } = _targetCtx;
expect(_isTextarea).toBe(isTextarea);
expect(_isBody).toBe(isBody);
expect(_windowElm).toBe(document.defaultView);
expect(_documentElm).toBe(document);
expect(_htmlElm).toBe(document.body.parentElement);
expect(_bodyElm).toBe(document.body);
expect(typeof _destroy).toBe('function');
const { _nativeScrollbarStyling, _cssCustomProperties, _getInitializationStrategy } = environment;
const {
_host: hostInitStrategy,
_viewport: viewportInitStrategy,
_padding: paddingInitStrategy,
_content: contentInitStrategy,
} = _getInitializationStrategy();
const inputIsElement = isHTMLElement(input);
const inputAsObj = input as StructureInitialization;
const styleElm = document.querySelector('style');
const checkStrategyDependendElements = (
elm: Element | null,
input: HTMLElement | boolean | undefined,
isStaticStrategy: boolean,
strategy: StructureInitializationStaticElement | StructureInitializationDynamicElement,
id: string
) => {
if (input) {
expect(elm).toBeTruthy();
} else {
if (input === false) {
expect(elm).toBeFalsy();
}
if (input === undefined) {
if (isStaticStrategy) {
strategy = strategy as StructureInitializationStaticElement;
if (typeof strategy === 'function') {
const result = strategy(target);
if (result) {
expect(result).toBe(elm);
} else {
expect(elm).toBeTruthy();
}
} else {
expect(elm).toBeTruthy();
}
} else {
strategy = strategy as StructureInitializationDynamicElement;
const expectDefaultValue = () => {
if (id === 'padding') {
if (_nativeScrollbarStyling) {
expect(elm).toBeFalsy();
} else {
expect(elm).toBeTruthy();
}
} else if (id === 'content') {
expect(elm).toBeFalsy();
}
};
if (typeof strategy === 'function') {
const result = strategy(target);
const resultIsBoolean = typeof result === 'boolean';
if (resultIsBoolean) {
if (result) {
expect(elm).toBeTruthy();
} else {
expect(elm).toBeFalsy();
}
} else if (result) {
expect(elm).toBe(result);
} else {
expectDefaultValue();
}
} else {
const strategyIsBoolean = typeof strategy === 'boolean';
if (strategyIsBoolean) {
if (strategy) {
expect(elm).toBeTruthy();
} else {
expect(elm).toBeFalsy();
}
} else {
expectDefaultValue();
}
}
}
}
}
};
if (_nativeScrollbarStyling || _cssCustomProperties) {
expect(styleElm).toBeFalsy();
} else {
expect(styleElm).toBeTruthy();
}
if (inputIsElement) {
checkStrategyDependendElements(padding, undefined, false, paddingInitStrategy, 'padding');
checkStrategyDependendElements(content, undefined, false, contentInitStrategy, 'content');
checkStrategyDependendElements(viewport, undefined, true, viewportInitStrategy, 'viewport');
checkStrategyDependendElements(host, undefined, true, hostInitStrategy, 'host');
} else {
const {
padding: inputPadding,
content: inputContent,
viewport: inputViewport,
host: inputHost,
} = inputAsObj;
checkStrategyDependendElements(padding, inputPadding, false, paddingInitStrategy, 'padding');
checkStrategyDependendElements(content, inputContent, false, contentInitStrategy, 'content');
checkStrategyDependendElements(viewport, inputViewport, true, viewportInitStrategy, 'viewport');
checkStrategyDependendElements(host, inputHost, true, hostInitStrategy, 'host');
}
return setup;
};
const assertCorrectDestroy = (snapshot: string, setup: StructureSetup) => {
const { _destroy } = setup;
_destroy();
// remove empty class attr
const elms = document.querySelectorAll('*');
Array.from(elms).forEach((elm) => {
const classAttr = elm.getAttribute('class');
if (classAttr === '') {
elm.removeAttribute('class');
}
});
expect(snapshot).toBe(getSnapshot());
};
const env: Environment = jest.requireActual('environment').getEnvironment();
const envDefault = {
name: 'default',
env,
};
const envNativeScrollbarStyling = {
name: 'native scrollbar styling',
env: {
...env,
_nativeScrollbarStyling: true,
},
};
const envCssCustomProperties = {
name: 'custom css properties',
env: {
...env,
_cssCustomProperties: true,
},
};
const envInitStrategyMin = {
name: 'initialization strategy min',
env: {
...env,
_getInitializationStrategy: () => ({
_host: null,
_viewport: () => null,
_content: () => false,
_padding: false,
_scrollbarsSlot: null,
}),
},
};
const envInitStrategyMax = {
name: 'initialization strategy max',
env: {
...env,
_getInitializationStrategy: () => ({
_host: null,
_viewport: null,
_content: true,
_padding: () => true,
_scrollbarsSlot: null,
}),
},
};
const envInitStrategyAssigned = {
name: 'initialization strategy assigned',
env: {
...env,
_getInitializationStrategy: () => ({
_host: () => document.querySelector('#host1') as HTMLElement,
_viewport: (target: HTMLElement) => target.querySelector('#viewport') as HTMLElement,
_content: (target: HTMLElement) => target.querySelector('#content') as HTMLElement,
_padding: (target: HTMLElement) => target.querySelector('#padding') as HTMLElement,
_scrollbarsSlot: null,
}),
},
};
describe('structureSetup', () => {
afterEach(() => clearBody());
[
envDefault,
envNativeScrollbarStyling,
envCssCustomProperties,
envInitStrategyMin,
envInitStrategyMax,
envInitStrategyAssigned,
].forEach((envWithName) => {
const { env: currEnv, name } = envWithName;
describe(`Environment: ${name}`, () => {
beforeAll(() => {
mockGetEnvironment.mockImplementation(() => currEnv);
});
[false, true].forEach((isTextarea) => {
describe(isTextarea ? 'textarea' : 'element', () => {
describe('basic', () => {
test('Element', () => {
const snapshot = fillBody(isTextarea);
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy(getTarget(isTextarea)),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
test('Object', () => {
const snapshot = fillBody(isTextarea);
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({ target: getTarget(isTextarea) }),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
});
describe('complex', () => {
describe('single assigned', () => {
test('padding', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="padding">${content}</div></div>`;
});
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
host: document.querySelector<HTMLElement>('#host')!,
target: getTarget(isTextarea),
padding: document.querySelector<HTMLElement>('#padding')!,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
test('viewport', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="viewport">${content}</div></div>`;
});
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
host: document.querySelector<HTMLElement>('#host')!,
target: getTarget(isTextarea),
viewport: document.querySelector<HTMLElement>('#viewport')!,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
test('content', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="content">${content}</div></div>`;
});
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
host: document.querySelector<HTMLElement>('#host')!,
target: getTarget(isTextarea),
content: document.querySelector<HTMLElement>('#content')!,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
});
describe('multiple assigned', () => {
test('padding viewport content', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="padding"><div id="viewport"><div id="content">${content}</div></div></div></div>`;
});
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
host: document.querySelector<HTMLElement>('#host')!,
target: getTarget(isTextarea),
padding: document.querySelector<HTMLElement>('#padding')!,
viewport: document.querySelector<HTMLElement>('#viewport')!,
content: document.querySelector<HTMLElement>('#content')!,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
test('padding viewport', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="padding"><div id="viewport">${content}</div></div></div>`;
});
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
host: document.querySelector<HTMLElement>('#host')!,
target: getTarget(isTextarea),
padding: document.querySelector<HTMLElement>('#padding')!,
viewport: document.querySelector<HTMLElement>('#viewport')!,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
test('padding content', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="padding"><div id="content">${content}</div></div></div>`;
});
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
host: document.querySelector<HTMLElement>('#host')!,
target: getTarget(isTextarea),
padding: document.querySelector<HTMLElement>('#padding')!,
content: document.querySelector<HTMLElement>('#content')!,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
test('viewport content', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="viewport"><div id="content">${content}</div></div></div>`;
});
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
host: document.querySelector<HTMLElement>('#host')!,
target: getTarget(isTextarea),
viewport: document.querySelector<HTMLElement>('#viewport')!,
content: document.querySelector<HTMLElement>('#content')!,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
});
describe('single false', () => {
test('padding', () => {
const snapshot = fillBody(isTextarea);
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
target: getTarget(isTextarea),
padding: false,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
test('content', () => {
const snapshot = fillBody(isTextarea);
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
target: getTarget(isTextarea),
content: false,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
});
describe('single true', () => {
test('padding', () => {
const snapshot = fillBody(isTextarea);
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
target: getTarget(isTextarea),
padding: true,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
test('content', () => {
const snapshot = fillBody(isTextarea);
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
target: getTarget(isTextarea),
content: true,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
});
describe('multiple false', () => {
test('padding & content', () => {
const snapshot = fillBody(isTextarea);
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
target: getTarget(isTextarea),
padding: false,
content: false,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
});
describe('multiple true', () => {
test('padding & content', () => {
const snapshot = fillBody(isTextarea);
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
target: getTarget(isTextarea),
padding: true,
content: true,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
});
describe('mixed', () => {
test('false: padding & content | assigned: viewport', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="viewport">${content}</div></div>`;
});
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
host: document.querySelector<HTMLElement>('#host')!,
target: getTarget(isTextarea),
padding: false,
viewport: document.querySelector<HTMLElement>('#viewport')!,
content: false,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
test('true: padding & content | assigned: viewport', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="viewport">${content}</div></div>`;
});
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
host: document.querySelector<HTMLElement>('#host')!,
target: getTarget(isTextarea),
padding: true,
viewport: document.querySelector<HTMLElement>('#viewport')!,
content: true,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
test('true: content | false: padding | assigned: viewport', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="viewport">${content}</div></div>`;
});
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
host: document.querySelector<HTMLElement>('#host')!,
target: getTarget(isTextarea),
padding: false,
viewport: document.querySelector<HTMLElement>('#viewport')!,
content: true,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
test('true: padding | false: content | assigned: viewport', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="viewport">${content}</div></div>`;
});
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
host: document.querySelector<HTMLElement>('#host')!,
target: getTarget(isTextarea),
padding: true,
viewport: document.querySelector<HTMLElement>('#viewport')!,
content: false,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
test('false: padding | assigned: content', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="content">${content}</div></div>`;
});
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
host: document.querySelector<HTMLElement>('#host')!,
target: getTarget(isTextarea),
padding: false,
content: document.querySelector<HTMLElement>('#content')!,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
test('true: padding | assigned: content', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="content">${content}</div></div>`;
});
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
host: document.querySelector<HTMLElement>('#host')!,
target: getTarget(isTextarea),
padding: true,
content: document.querySelector<HTMLElement>('#content')!,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
test('false: padding | assigned: viewport', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="viewport">${content}</div></div>`;
});
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
host: document.querySelector<HTMLElement>('#host')!,
target: getTarget(isTextarea),
padding: false,
viewport: document.querySelector<HTMLElement>('#viewport')!,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
test('true: padding | assigned: viewport', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="viewport">${content}</div></div>`;
});
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
host: document.querySelector<HTMLElement>('#host')!,
target: getTarget(isTextarea),
padding: true,
viewport: document.querySelector<HTMLElement>('#viewport')!,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
test('false: padding | assigned: viewport & content', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="viewport"><div id="content">${content}</div></div></div>`;
});
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
host: document.querySelector<HTMLElement>('#host')!,
target: getTarget(isTextarea),
viewport: document.querySelector<HTMLElement>('#viewport')!,
padding: false,
content: document.querySelector<HTMLElement>('#content')!,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
test('true: padding | assigned: viewport & content', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="viewport"><div id="content">${content}</div></div></div>`;
});
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
host: document.querySelector<HTMLElement>('#host')!,
target: getTarget(isTextarea),
viewport: document.querySelector<HTMLElement>('#viewport')!,
padding: true,
content: document.querySelector<HTMLElement>('#content')!,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
test('false: content | assigned: padding', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="padding">${content}</div></div>`;
});
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
host: document.querySelector<HTMLElement>('#host')!,
target: getTarget(isTextarea),
padding: document.querySelector<HTMLElement>('#padding')!,
content: false,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
test('true: content | assigned: padding', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="padding">${content}</div></div>`;
});
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
host: document.querySelector<HTMLElement>('#host')!,
target: getTarget(isTextarea),
padding: document.querySelector<HTMLElement>('#padding')!,
content: true,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
test('false: content | assigned: viewport', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="viewport">${content}</div></div>`;
});
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
host: document.querySelector<HTMLElement>('#host')!,
target: getTarget(isTextarea),
viewport: document.querySelector<HTMLElement>('#viewport')!,
content: false,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
test('true: content | assigned: viewport', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="viewport">${content}</div></div>`;
});
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
host: document.querySelector<HTMLElement>('#host')!,
target: getTarget(isTextarea),
viewport: document.querySelector<HTMLElement>('#viewport')!,
content: true,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
test('false: content | assigned: padding & viewport', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="padding"><div id="viewport">${content}</div></div></div>`;
});
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
host: document.querySelector<HTMLElement>('#host')!,
target: getTarget(isTextarea),
padding: document.querySelector<HTMLElement>('#padding')!,
viewport: document.querySelector<HTMLElement>('#viewport')!,
content: false,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
test('true: content | assigned: padding & viewport', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="padding"><div id="viewport">${content}</div></div></div>`;
});
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
host: document.querySelector<HTMLElement>('#host')!,
target: getTarget(isTextarea),
padding: document.querySelector<HTMLElement>('#padding')!,
viewport: document.querySelector<HTMLElement>('#viewport')!,
content: true,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
});
});
});
});
});
});
});
@@ -1,870 +0,0 @@
import { Environment, StructureInitializationStaticElement, StructureInitializationDynamicElement } from 'environment';
import { OSTarget, StructureInitialization } from 'typings';
import { createStructureSetup, StructureSetup } from 'setups/structureSetup';
import { isHTMLElement } from 'support';
const mockGetEnvironment = jest.fn();
jest.mock('environment', () => {
return {
getEnvironment: jest.fn().mockImplementation(() => mockGetEnvironment()),
};
});
interface StructureSetupProxy {
input: OSTarget | StructureInitialization;
setup: StructureSetup;
}
const textareaId = 'textarea';
const textareaHostId = 'host';
const elementId = 'target';
const dynamicContent = 'text<p>paragraph</p>';
const textareaContent = `<textarea id="${textareaId}">text</textarea>`;
const getSnapshot = () => document.body.innerHTML;
const getTarget = (textarea?: boolean) => document.getElementById(textarea ? textareaId : elementId)!;
const fillBody = (textarea?: boolean, customDOM?: (content: string, hostId: string) => string) => {
document.body.innerHTML = `
<nav></nav>
${
customDOM
? customDOM(textarea ? textareaContent : dynamicContent, textarea ? textareaHostId : elementId)
: textarea
? textareaContent
: `<div id="${elementId}">${dynamicContent}</div>`
}
<footer></footer>
`;
return getSnapshot();
};
const clearBody = () => {
document.body.innerHTML = '';
};
const getElements = (textarea?: boolean) => {
const target = getTarget(textarea);
const host = document.querySelector('.os-host')!;
const padding = document.querySelector('.os-padding')!;
const viewport = document.querySelector('.os-viewport')!;
const content = document.querySelector('.os-content')!;
return {
target,
host,
padding,
viewport,
content,
};
};
const assertCorrectDOMStructure = (textarea?: boolean) => {
const { target, host, padding, viewport, content } = getElements(textarea);
expect(host).toBeTruthy();
expect(viewport).toBeTruthy();
expect(viewport.parentElement).toBe(padding || host);
if (content) {
expect(content.parentElement).toBe(viewport);
}
if (padding) {
expect(padding.parentElement).toBe(host);
}
expect(host.parentElement).toBe(document.body);
expect(host.previousElementSibling).toBe(document.querySelector('nav'));
expect(host.nextElementSibling).toBe(document.querySelector('footer'));
const contentElm = content || viewport;
if (textarea) {
expect(target.parentElement).toBe(contentElm);
expect(contentElm.innerHTML).toBe(textareaContent);
} else {
expect(target).toBe(host);
expect(contentElm.innerHTML).toBe(dynamicContent);
}
};
const createStructureSetupProxy = (target: OSTarget | StructureInitialization): StructureSetupProxy => ({
input: target,
setup: createStructureSetup(target),
});
const assertCorrectSetup = (textarea: boolean, setupProxy: StructureSetupProxy, environment: Environment): StructureSetup => {
const { input, setup } = setupProxy;
const { _targetObj, _targetCtx, _destroy } = setup;
const { _target, _host, _padding, _viewport, _content } = _targetObj;
const { target, host, padding, viewport, content } = getElements(textarea);
const isTextarea = target.matches('textarea');
const isBody = target.matches('body');
expect(textarea).toBe(isTextarea);
expect(_target).toBe(target);
expect(_host).toBe(host);
if (padding || _padding) {
expect(_padding).toBe(padding);
} else {
expect(padding).toBeFalsy();
expect(_padding).toBeFalsy();
}
if (viewport || _viewport) {
expect(_viewport).toBe(viewport);
} else {
expect(viewport).toBeFalsy();
expect(_viewport).toBeFalsy();
}
if (content || _content) {
expect(_content).toBe(content);
} else {
expect(content).toBeFalsy();
expect(_content).toBeFalsy();
}
const { _isTextarea, _isBody, _bodyElm, _htmlElm, _documentElm, _windowElm } = _targetCtx;
expect(_isTextarea).toBe(isTextarea);
expect(_isBody).toBe(isBody);
expect(_windowElm).toBe(document.defaultView);
expect(_documentElm).toBe(document);
expect(_htmlElm).toBe(document.body.parentElement);
expect(_bodyElm).toBe(document.body);
expect(typeof _destroy).toBe('function');
const { _nativeScrollbarStyling, _cssCustomProperties, _getInitializationStrategy } = environment;
const {
_host: hostInitStrategy,
_viewport: viewportInitStrategy,
_padding: paddingInitStrategy,
_content: contentInitStrategy,
} = _getInitializationStrategy();
const inputIsElement = isHTMLElement(input);
const inputAsObj = input as StructureInitialization;
const styleElm = document.querySelector('style');
const checkStrategyDependendElements = (
elm: Element | null,
input: HTMLElement | boolean | undefined,
isStaticStrategy: boolean,
strategy: StructureInitializationStaticElement | StructureInitializationDynamicElement,
id: string
) => {
if (input) {
expect(elm).toBeTruthy();
} else {
if (input === false) {
expect(elm).toBeFalsy();
}
if (input === undefined) {
if (isStaticStrategy) {
strategy = strategy as StructureInitializationStaticElement;
if (typeof strategy === 'function') {
const result = strategy(target);
if (result) {
expect(result).toBe(elm);
} else {
expect(elm).toBeTruthy();
}
} else {
expect(elm).toBeTruthy();
}
} else {
strategy = strategy as StructureInitializationDynamicElement;
const expectDefaultValue = () => {
if (id === 'padding') {
if (_nativeScrollbarStyling) {
expect(elm).toBeFalsy();
} else {
expect(elm).toBeTruthy();
}
} else if (id === 'content') {
expect(elm).toBeFalsy();
}
};
if (typeof strategy === 'function') {
const result = strategy(target);
const resultIsBoolean = typeof result === 'boolean';
if (resultIsBoolean) {
if (result) {
expect(elm).toBeTruthy();
} else {
expect(elm).toBeFalsy();
}
} else if (result) {
expect(elm).toBe(result);
} else {
expectDefaultValue();
}
} else {
const strategyIsBoolean = typeof strategy === 'boolean';
if (strategyIsBoolean) {
if (strategy) {
expect(elm).toBeTruthy();
} else {
expect(elm).toBeFalsy();
}
} else {
expectDefaultValue();
}
}
}
}
}
};
if (_nativeScrollbarStyling || _cssCustomProperties) {
expect(styleElm).toBeFalsy();
} else {
expect(styleElm).toBeTruthy();
}
if (inputIsElement) {
checkStrategyDependendElements(padding, undefined, false, paddingInitStrategy, 'padding');
checkStrategyDependendElements(content, undefined, false, contentInitStrategy, 'content');
checkStrategyDependendElements(viewport, undefined, true, viewportInitStrategy, 'viewport');
checkStrategyDependendElements(host, undefined, true, hostInitStrategy, 'host');
} else {
const { padding: inputPadding, content: inputContent, viewport: inputViewport, host: inputHost } = inputAsObj;
checkStrategyDependendElements(padding, inputPadding, false, paddingInitStrategy, 'padding');
checkStrategyDependendElements(content, inputContent, false, contentInitStrategy, 'content');
checkStrategyDependendElements(viewport, inputViewport, true, viewportInitStrategy, 'viewport');
checkStrategyDependendElements(host, inputHost, true, hostInitStrategy, 'host');
}
return setup;
};
const assertCorrectDestroy = (snapshot: string, setup: StructureSetup) => {
const { _destroy } = setup;
_destroy();
// remove empty class attr
const elms = document.querySelectorAll('*');
Array.from(elms).forEach((elm) => {
const classAttr = elm.getAttribute('class');
if (classAttr === '') {
elm.removeAttribute('class');
}
});
expect(snapshot).toBe(getSnapshot());
};
const env: Environment = jest.requireActual('environment').getEnvironment();
const envDefault = {
name: 'default',
env: env,
};
const envNativeScrollbarStyling = {
name: 'native scrollbar styling',
env: {
...env,
_nativeScrollbarStyling: true,
},
};
const envCssCustomProperties = {
name: 'custom css properties',
env: {
...env,
_cssCustomProperties: true,
},
};
const envInitStrategyMin = {
name: 'initialization strategy min',
env: {
...env,
_getInitializationStrategy: () => ({
_host: null,
_viewport: () => null,
_content: () => false,
_padding: false,
_scrollbarsSlot: null,
}),
},
};
const envInitStrategyMax = {
name: 'initialization strategy max',
env: {
...env,
_getInitializationStrategy: () => ({
_host: null,
_viewport: null,
_content: true,
_padding: () => true,
_scrollbarsSlot: null,
}),
},
};
const envInitStrategyAssigned = {
name: 'initialization strategy assigned',
env: {
...env,
_getInitializationStrategy: () => ({
_host: () => document.querySelector('#host1') as HTMLElement,
_viewport: (target: HTMLElement) => target.querySelector('#viewport') as HTMLElement,
_content: (target: HTMLElement) => target.querySelector('#content') as HTMLElement,
_padding: (target: HTMLElement) => target.querySelector('#padding') as HTMLElement,
_scrollbarsSlot: null,
}),
},
};
describe('structureSetup', () => {
afterEach(() => clearBody());
[envDefault, envNativeScrollbarStyling, envCssCustomProperties, envInitStrategyMin, envInitStrategyMax, envInitStrategyAssigned].forEach(
(envWithName) => {
const { env: currEnv, name } = envWithName;
describe(`Environment: ${name}`, () => {
beforeAll(() => {
mockGetEnvironment.mockImplementation(() => currEnv);
});
[false, true].forEach((isTextarea) => {
describe(isTextarea ? 'textarea' : 'element', () => {
describe('basic', () => {
test('Element', () => {
const snapshot = fillBody(isTextarea);
const setup = assertCorrectSetup(isTextarea, createStructureSetupProxy(getTarget(isTextarea)), currEnv);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
test('Object', () => {
const snapshot = fillBody(isTextarea);
const setup = assertCorrectSetup(isTextarea, createStructureSetupProxy({ target: getTarget(isTextarea) }), currEnv);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
});
describe('complex', () => {
describe('single assigned', () => {
test('padding', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="padding">${content}</div></div>`;
});
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
host: document.querySelector<HTMLElement>('#host')!,
target: getTarget(isTextarea),
padding: document.querySelector<HTMLElement>('#padding')!,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
test('viewport', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="viewport">${content}</div></div>`;
});
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
host: document.querySelector<HTMLElement>('#host')!,
target: getTarget(isTextarea),
viewport: document.querySelector<HTMLElement>('#viewport')!,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
test('content', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="content">${content}</div></div>`;
});
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
host: document.querySelector<HTMLElement>('#host')!,
target: getTarget(isTextarea),
content: document.querySelector<HTMLElement>('#content')!,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
});
describe('multiple assigned', () => {
test('padding viewport content', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="padding"><div id="viewport"><div id="content">${content}</div></div></div></div>`;
});
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
host: document.querySelector<HTMLElement>('#host')!,
target: getTarget(isTextarea),
padding: document.querySelector<HTMLElement>('#padding')!,
viewport: document.querySelector<HTMLElement>('#viewport')!,
content: document.querySelector<HTMLElement>('#content')!,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
test('padding viewport', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="padding"><div id="viewport">${content}</div></div></div>`;
});
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
host: document.querySelector<HTMLElement>('#host')!,
target: getTarget(isTextarea),
padding: document.querySelector<HTMLElement>('#padding')!,
viewport: document.querySelector<HTMLElement>('#viewport')!,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
test('padding content', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="padding"><div id="content">${content}</div></div></div>`;
});
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
host: document.querySelector<HTMLElement>('#host')!,
target: getTarget(isTextarea),
padding: document.querySelector<HTMLElement>('#padding')!,
content: document.querySelector<HTMLElement>('#content')!,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
test('viewport content', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="viewport"><div id="content">${content}</div></div></div>`;
});
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
host: document.querySelector<HTMLElement>('#host')!,
target: getTarget(isTextarea),
viewport: document.querySelector<HTMLElement>('#viewport')!,
content: document.querySelector<HTMLElement>('#content')!,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
});
describe('single false', () => {
test('padding', () => {
const snapshot = fillBody(isTextarea);
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
target: getTarget(isTextarea),
padding: false,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
test('content', () => {
const snapshot = fillBody(isTextarea);
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
target: getTarget(isTextarea),
content: false,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
});
describe('single true', () => {
test('padding', () => {
const snapshot = fillBody(isTextarea);
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
target: getTarget(isTextarea),
padding: true,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
test('content', () => {
const snapshot = fillBody(isTextarea);
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
target: getTarget(isTextarea),
content: true,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
});
describe('multiple false', () => {
test('padding & content', () => {
const snapshot = fillBody(isTextarea);
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
target: getTarget(isTextarea),
padding: false,
content: false,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
});
describe('multiple true', () => {
test('padding & content', () => {
const snapshot = fillBody(isTextarea);
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
target: getTarget(isTextarea),
padding: true,
content: true,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
});
describe('mixed', () => {
test('false: padding & content | assigned: viewport', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="viewport">${content}</div></div>`;
});
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
host: document.querySelector<HTMLElement>('#host')!,
target: getTarget(isTextarea),
padding: false,
viewport: document.querySelector<HTMLElement>('#viewport')!,
content: false,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
test('true: padding & content | assigned: viewport', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="viewport">${content}</div></div>`;
});
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
host: document.querySelector<HTMLElement>('#host')!,
target: getTarget(isTextarea),
padding: true,
viewport: document.querySelector<HTMLElement>('#viewport')!,
content: true,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
test('true: content | false: padding | assigned: viewport', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="viewport">${content}</div></div>`;
});
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
host: document.querySelector<HTMLElement>('#host')!,
target: getTarget(isTextarea),
padding: false,
viewport: document.querySelector<HTMLElement>('#viewport')!,
content: true,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
test('true: padding | false: content | assigned: viewport', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="viewport">${content}</div></div>`;
});
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
host: document.querySelector<HTMLElement>('#host')!,
target: getTarget(isTextarea),
padding: true,
viewport: document.querySelector<HTMLElement>('#viewport')!,
content: false,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
test('false: padding | assigned: content', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="content">${content}</div></div>`;
});
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
host: document.querySelector<HTMLElement>('#host')!,
target: getTarget(isTextarea),
padding: false,
content: document.querySelector<HTMLElement>('#content')!,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
test('true: padding | assigned: content', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="content">${content}</div></div>`;
});
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
host: document.querySelector<HTMLElement>('#host')!,
target: getTarget(isTextarea),
padding: true,
content: document.querySelector<HTMLElement>('#content')!,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
test('false: padding | assigned: viewport', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="viewport">${content}</div></div>`;
});
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
host: document.querySelector<HTMLElement>('#host')!,
target: getTarget(isTextarea),
padding: false,
viewport: document.querySelector<HTMLElement>('#viewport')!,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
test('true: padding | assigned: viewport', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="viewport">${content}</div></div>`;
});
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
host: document.querySelector<HTMLElement>('#host')!,
target: getTarget(isTextarea),
padding: true,
viewport: document.querySelector<HTMLElement>('#viewport')!,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
test('false: padding | assigned: viewport & content', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="viewport"><div id="content">${content}</div></div></div>`;
});
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
host: document.querySelector<HTMLElement>('#host')!,
target: getTarget(isTextarea),
viewport: document.querySelector<HTMLElement>('#viewport')!,
padding: false,
content: document.querySelector<HTMLElement>('#content')!,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
test('true: padding | assigned: viewport & content', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="viewport"><div id="content">${content}</div></div></div>`;
});
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
host: document.querySelector<HTMLElement>('#host')!,
target: getTarget(isTextarea),
viewport: document.querySelector<HTMLElement>('#viewport')!,
padding: true,
content: document.querySelector<HTMLElement>('#content')!,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
test('false: content | assigned: padding', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="padding">${content}</div></div>`;
});
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
host: document.querySelector<HTMLElement>('#host')!,
target: getTarget(isTextarea),
padding: document.querySelector<HTMLElement>('#padding')!,
content: false,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
test('true: content | assigned: padding', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="padding">${content}</div></div>`;
});
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
host: document.querySelector<HTMLElement>('#host')!,
target: getTarget(isTextarea),
padding: document.querySelector<HTMLElement>('#padding')!,
content: true,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
test('false: content | assigned: viewport', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="viewport">${content}</div></div>`;
});
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
host: document.querySelector<HTMLElement>('#host')!,
target: getTarget(isTextarea),
viewport: document.querySelector<HTMLElement>('#viewport')!,
content: false,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
test('true: content | assigned: viewport', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="viewport">${content}</div></div>`;
});
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
host: document.querySelector<HTMLElement>('#host')!,
target: getTarget(isTextarea),
viewport: document.querySelector<HTMLElement>('#viewport')!,
content: true,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
test('false: content | assigned: padding & viewport', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="padding"><div id="viewport">${content}</div></div></div>`;
});
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
host: document.querySelector<HTMLElement>('#host')!,
target: getTarget(isTextarea),
padding: document.querySelector<HTMLElement>('#padding')!,
viewport: document.querySelector<HTMLElement>('#viewport')!,
content: false,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
test('true: content | assigned: padding & viewport', () => {
const snapshot = fillBody(isTextarea, (content, hostId) => {
return `<div id="${hostId}"><div id="padding"><div id="viewport">${content}</div></div></div>`;
});
const setup = assertCorrectSetup(
isTextarea,
createStructureSetupProxy({
host: document.querySelector<HTMLElement>('#host')!,
target: getTarget(isTextarea),
padding: document.querySelector<HTMLElement>('#padding')!,
viewport: document.querySelector<HTMLElement>('#viewport')!,
content: true,
}),
currEnv
);
assertCorrectDOMStructure(isTextarea);
assertCorrectDestroy(snapshot, setup);
});
});
});
});
});
});
}
);
});
@@ -7,7 +7,17 @@ import { setTestResult, waitForOrFailTest } from '@/testing-browser/TestResult';
import { generateClassChangeSelectCallback, iterateSelect } from '@/testing-browser/Select';
import { timeout } from '@/testing-browser/timeout';
import { OverlayScrollbars } from 'overlayscrollbars';
import { assignDeep, clientSize, from, getBoundingClientRect, style, parent, addClass, WH, removeAttr } from 'support';
import {
assignDeep,
clientSize,
from,
getBoundingClientRect,
style,
parent,
addClass,
WH,
removeAttr,
} from 'support';
interface Metrics {
offset: {
@@ -74,8 +84,12 @@ const getMetrics = (elm: HTMLElement): Metrics => {
return {
offset: {
left: rounding(comparisonBCR.left - comparisonEnvBCR.left).toFixed(Math.min(fixedDigitsOffset, fixedDigits)),
top: rounding(comparisonBCR.top - comparisonEnvBCR.top).toFixed(Math.min(fixedDigitsOffset, fixedDigits)),
left: rounding(comparisonBCR.left - comparisonEnvBCR.left).toFixed(
Math.min(fixedDigitsOffset, fixedDigits)
),
top: rounding(comparisonBCR.top - comparisonEnvBCR.top).toFixed(
Math.min(fixedDigitsOffset, fixedDigits)
),
},
size: {
width: rounding(comparisonBCR.width).toFixed(fixedDigits),
@@ -166,11 +180,11 @@ target!.querySelector('.os-viewport')?.addEventListener('scroll', (e) => {
resize(target!).addResizeListener((width, height) => {
style(comparison, { width, height });
});
//resize(comparison!).addResizeListener((width, height) => style(target, { width, height }));
// resize(comparison!).addResizeListener((width, height) => style(target, { width, height }));
resize(targetResize!).addResizeListener((width, height) => {
style(comparisonResize, { width, height });
});
//resize(comparisonRes!).addResizeListener((width, height) => style(targetRes, { width, height }));
// resize(comparisonRes!).addResizeListener((width, height) => style(targetRes, { width, height }));
const selectCallbackEnv = generateClassChangeSelectCallback(from(envElms));
const envWidthSelect = document.querySelector<HTMLSelectElement>('#envWidth');
@@ -230,58 +244,136 @@ const checkMetrics = async (checkComparison: CheckComparisonObj) => {
if (isFractionalPixelRatio()) {
should.ok(
plusMinusArr(targetMetrics.scroll.width, fractionalPixelRatioTollerance).indexOf(comparisonMetrics.scroll.width) > -1,
plusMinusArr(targetMetrics.scroll.width, fractionalPixelRatioTollerance).indexOf(
comparisonMetrics.scroll.width
) > -1,
`Scroll width equality. (+-${fractionalPixelRatioTollerance}) | Target: ${targetMetrics.scroll.width} | Comparison: ${comparisonMetrics.scroll.width}`
);
should.ok(
plusMinusArr(targetMetrics.scroll.height, fractionalPixelRatioTollerance).indexOf(comparisonMetrics.scroll.height) > -1,
plusMinusArr(targetMetrics.scroll.height, fractionalPixelRatioTollerance).indexOf(
comparisonMetrics.scroll.height
) > -1,
`Scroll height equality. (+-${fractionalPixelRatioTollerance}) | Target: ${targetMetrics.scroll.height} | Comparison: ${comparisonMetrics.scroll.height}`
);
should.ok(
plusMinusArr(osInstance.state()._overflowAmount.w, fractionalPixelRatioTollerance).indexOf(comparisonMetrics.scroll.width) > -1,
`Overflow amount width equality. (+-${fractionalPixelRatioTollerance}) | Amount: ${osInstance.state()._overflowAmount.w} | Comparison: ${
plusMinusArr(osInstance.state()._overflowAmount.w, fractionalPixelRatioTollerance).indexOf(
comparisonMetrics.scroll.width
}`
) > -1,
`Overflow amount width equality. (+-${fractionalPixelRatioTollerance}) | Amount: ${
osInstance.state()._overflowAmount.w
} | Comparison: ${comparisonMetrics.scroll.width}`
);
should.ok(
plusMinusArr(osInstance.state()._overflowAmount.h, fractionalPixelRatioTollerance).indexOf(comparisonMetrics.scroll.height) > -1,
`Overflow amount height equality. (+-${fractionalPixelRatioTollerance}) | Amount: ${osInstance.state()._overflowAmount.h} | Comparison: ${
plusMinusArr(osInstance.state()._overflowAmount.h, fractionalPixelRatioTollerance).indexOf(
comparisonMetrics.scroll.height
}`
) > -1,
`Overflow amount height equality. (+-${fractionalPixelRatioTollerance}) | Amount: ${
osInstance.state()._overflowAmount.h
} | Comparison: ${comparisonMetrics.scroll.height}`
);
} else {
should.equal(targetMetrics.scroll.width, comparisonMetrics.scroll.width, 'Scroll width equality.');
should.equal(targetMetrics.scroll.height, comparisonMetrics.scroll.height, 'Scroll height equality.');
should.equal(
targetMetrics.scroll.width,
comparisonMetrics.scroll.width,
'Scroll width equality.'
);
should.equal(
targetMetrics.scroll.height,
comparisonMetrics.scroll.height,
'Scroll height equality.'
);
should.equal(osInstance.state()._overflowAmount.w, comparisonMetrics.scroll.width, 'Overflow amount width equality.');
should.equal(osInstance.state()._overflowAmount.h, comparisonMetrics.scroll.height, 'Overflow amount height equality.');
should.equal(
osInstance.state()._overflowAmount.w,
comparisonMetrics.scroll.width,
'Overflow amount width equality.'
);
should.equal(
osInstance.state()._overflowAmount.h,
comparisonMetrics.scroll.height,
'Overflow amount height equality.'
);
should.equal(targetMetrics.hasOverflow.x, comparisonMetrics.hasOverflow.x, 'Has overflow x equality.');
should.equal(targetMetrics.hasOverflow.y, comparisonMetrics.hasOverflow.y, 'Has overflow y equality.');
should.equal(
targetMetrics.hasOverflow.x,
comparisonMetrics.hasOverflow.x,
'Has overflow x equality.'
);
should.equal(
targetMetrics.hasOverflow.y,
comparisonMetrics.hasOverflow.y,
'Has overflow y equality.'
);
}
if (targetMetrics.hasOverflow.x) {
should.equal(style(targetViewport!, 'overflowX'), 'scroll', 'Overflow-X should result in scroll.');
should.ok(osInstance.state()._overflowAmount.w > 0, 'Overflow amount width should be > 0 with overflow.');
should.equal(
style(targetViewport!, 'overflowX'),
'scroll',
'Overflow-X should result in scroll.'
);
should.ok(
osInstance.state()._overflowAmount.w > 0,
'Overflow amount width should be > 0 with overflow.'
);
} else {
should.notEqual(style(targetViewport!, 'overflowX'), 'scroll', 'No Overflow-X shouldnt result in scroll.');
should.equal(osInstance.state()._overflowAmount.w, 0, 'Overflow amount width should be 0 without overflow.');
should.notEqual(
style(targetViewport!, 'overflowX'),
'scroll',
'No Overflow-X shouldnt result in scroll.'
);
should.equal(
osInstance.state()._overflowAmount.w,
0,
'Overflow amount width should be 0 without overflow.'
);
}
if (targetMetrics.hasOverflow.y) {
should.equal(style(targetViewport!, 'overflowY'), 'scroll', 'Overflow-Y should result in scroll.');
should.ok(osInstance.state()._overflowAmount.h > 0, 'Overflow amount height should be > 0 with overflow.');
should.equal(
style(targetViewport!, 'overflowY'),
'scroll',
'Overflow-Y should result in scroll.'
);
should.ok(
osInstance.state()._overflowAmount.h > 0,
'Overflow amount height should be > 0 with overflow.'
);
} else {
should.notEqual(style(targetViewport!, 'overflowY'), 'scroll', 'No Overflow-Y shouldnt result in scroll.');
should.equal(osInstance.state()._overflowAmount.h, 0, 'Overflow amount height should be 0 without overflow.');
should.notEqual(
style(targetViewport!, 'overflowY'),
'scroll',
'No Overflow-Y shouldnt result in scroll.'
);
should.equal(
osInstance.state()._overflowAmount.h,
0,
'Overflow amount height should be 0 without overflow.'
);
}
should.equal(targetMetrics.percentElm.width, comparisonMetrics.percentElm.width, 'Percent Elements width equality.');
should.equal(targetMetrics.percentElm.height, comparisonMetrics.percentElm.height, 'Percent Elements height equality.');
should.equal(
targetMetrics.percentElm.width,
comparisonMetrics.percentElm.width,
'Percent Elements width equality.'
);
should.equal(
targetMetrics.percentElm.height,
comparisonMetrics.percentElm.height,
'Percent Elements height equality.'
);
should.equal(targetMetrics.endElm.width, comparisonMetrics.endElm.width, 'End Elements width equality.');
should.equal(targetMetrics.endElm.height, comparisonMetrics.endElm.height, 'End Elements height equality.');
should.equal(
targetMetrics.endElm.width,
comparisonMetrics.endElm.width,
'End Elements width equality.'
);
should.equal(
targetMetrics.endElm.height,
comparisonMetrics.endElm.height,
'End Elements height equality.'
);
await timeout(1);
@@ -307,31 +399,36 @@ const iterate = async (select: HTMLSelectElement | null, afterEach?: () => any)
afterEach,
});
};
/*
const iterateEnvWidth = async (afterEach?: () => any) => {
await iterate(envWidthSelect, afterEach);
};
const iterateEnvHeight = async (afterEach?: () => any) => {
await iterate(envHeightSelect, afterEach);
};
*/
const iterateHeight = async (afterEach?: () => any) => {
await iterate(containerHeightSelect, afterEach);
};
const iterateWidth = async (afterEach?: () => any) => {
await iterate(containerWidthSelect, afterEach);
};
/*
const iterateFloat = async (afterEach?: () => any) => {
await iterate(containerFloatSelect, afterEach);
};
*/
const iteratePadding = async (afterEach?: () => any) => {
await iterate(containerPaddingSelect, afterEach);
};
const iterateBorder = async (afterEach?: () => any) => {
await iterate(containerBorderSelect, afterEach);
};
/*
const iterateMargin = async (afterEach?: () => any) => {
await iterate(containerMarginSelect, afterEach);
};
*/
const iterateBoxSizing = async (afterEach?: () => any) => {
await iterate(containerBoxSizingSelect, afterEach);
};
@@ -354,8 +451,10 @@ const overflowTest = async () => {
const computedStyle = window.getComputedStyle(elm);
const size = clientSize(elm);
return {
w: size.w - (parseFloat(computedStyle.paddingLeft) + parseFloat(computedStyle.paddingRight)),
h: size.h - (parseFloat(computedStyle.paddingTop) + parseFloat(computedStyle.paddingBottom)),
w:
size.w - (parseFloat(computedStyle.paddingLeft) + parseFloat(computedStyle.paddingRight)),
h:
size.h - (parseFloat(computedStyle.paddingTop) + parseFloat(computedStyle.paddingBottom)),
};
}
@@ -425,15 +524,29 @@ const overflowTest = async () => {
await waitForOrFailTest(() => {
if (width) {
should.ok(overflowAmountCheck.width >= addOverflow, 'Correct smallest possible overflow width. (?)');
should.ok(
overflowAmountCheck.width >= addOverflow,
'Correct smallest possible overflow width. (?)'
);
} else {
should.equal(overflowAmountCheck.width, 0, 'Correct smallest possible overflow width. (0)');
should.equal(
overflowAmountCheck.width,
0,
'Correct smallest possible overflow width. (0)'
);
}
if (height) {
should.ok(overflowAmountCheck.height >= addOverflow, 'Correct smallest possible overflow height. (?)');
should.ok(
overflowAmountCheck.height >= addOverflow,
'Correct smallest possible overflow height. (?)'
);
} else {
should.equal(overflowAmountCheck.height, 0, 'Correct smallest possible overflow height. (0)');
should.equal(
overflowAmountCheck.height,
0,
'Correct smallest possible overflow height. (0)'
);
}
});
@@ -0,0 +1,58 @@
// @ts-ignore
import { playwrightRollup, expectSuccess } from '@/playwright/rollup';
import { test, Page } from '@playwright/test';
playwrightRollup();
test.describe('StructureLifecycle', () => {
[false, true].forEach(async (nativeScrollbarStyling) => {
const withText = nativeScrollbarStyling ? 'with' : 'without';
const nss = async (page: Page) => {
if (!nativeScrollbarStyling) {
await page.click('#nss');
await page.waitForTimeout(500);
}
};
test.describe(`structureLifecycles ${withText} native scrollbar styling`, () => {
test.describe.configure({ mode: 'parallel' });
test('default', async ({ page }) => {
await nss(page);
await page.click('#start');
await expectSuccess(page);
});
test('without flexbox glue & css custom props', async ({ page }) => {
await nss(page);
await page.click('#fbg');
await page.waitForTimeout(500);
await page.click('#ccp');
await page.waitForTimeout(500);
await page.click('#start');
await expectSuccess(page);
});
test('with partially overlaid scrollbars', async ({ page, browserName }) => {
test.skip(
browserName === 'firefox' || browserName === 'webkit',
"firefox can't simulate partially overlaid scrollbars, boost speed by omitting webkit"
);
await nss(page);
await page.click('#po');
await page.waitForTimeout(500);
await page.click('#start');
await expectSuccess(page);
});
test('with fully overlaid scrollbars', async ({ page }) => {
await nss(page);
await page.click('#fo');
await page.waitForTimeout(500);
await page.click('#start');
await expectSuccess(page);
});
});
});
});
@@ -4,7 +4,19 @@ import should from 'should';
import { generateSelectCallback, iterateSelect } from '@/testing-browser/Select';
import { timeout } from '@/testing-browser/timeout';
import { setTestResult, waitForOrFailTest } from '@/testing-browser/TestResult';
import { appendChildren, createDiv, removeElements, children, isArray, isNumber, liesBetween, addClass, removeClass, diffClass, on } from 'support';
import {
appendChildren,
createDiv,
removeElements,
children,
isArray,
isNumber,
liesBetween,
addClass,
removeClass,
diffClass,
on,
} from 'support';
import { createDOMObserver } from 'observers/domObserver';
@@ -27,8 +39,12 @@ const targetElm: HTMLElement | null = document.querySelector('#target');
const trargetContentElm: HTMLElement | null = document.querySelector('#target .content');
const targetElmContentElm: HTMLElement | null = document.querySelector('#content-host');
const contentElmAttrChange: HTMLElement | null = document.querySelector('#target .content-nest');
const contentBetweenElmAttrChange: HTMLElement | null = document.querySelector('#content-host .padding-nest-item');
const contentHostElmAttrChange: HTMLElement | null = document.querySelector('#content-nest-item-host');
const contentBetweenElmAttrChange: HTMLElement | null = document.querySelector(
'#content-host .padding-nest-item'
);
const contentHostElmAttrChange: HTMLElement | null = document.querySelector(
'#content-nest-item-host'
);
const targetElmsSlot = document.querySelector('#target .host-nest-item');
const targetContentElmsSlot = document.querySelector('#target .content .content-nest');
@@ -36,20 +52,40 @@ const targetContentBetweenElmsSlot = document.querySelector('#content-host');
const imgElmsSlot = document.querySelector('#target .content-nest');
const transitionElmsSlot = document.querySelector('#content-host .content');
const addRemoveTargetElms: HTMLButtonElement | null = document.querySelector('#addRemoveTargetElms');
const addRemoveTargetContentElms: HTMLButtonElement | null = document.querySelector('#addRemoveTargetContentElms');
const addRemoveTargetContentBetweenElms: HTMLButtonElement | null = document.querySelector('#addRemoveTargetContentBetweenElms');
const addRemoveTargetElms: HTMLButtonElement | null = document.querySelector(
'#addRemoveTargetElms'
);
const addRemoveTargetContentElms: HTMLButtonElement | null = document.querySelector(
'#addRemoveTargetContentElms'
);
const addRemoveTargetContentBetweenElms: HTMLButtonElement | null = document.querySelector(
'#addRemoveTargetContentBetweenElms'
);
const addRemoveImgElms: HTMLButtonElement | null = document.querySelector('#addRemoveImgElms');
const addRemoveTransitionElms: HTMLButtonElement | null = document.querySelector('#addRemoveTransitionElms');
const addRemoveTransitionElms: HTMLButtonElement | null = document.querySelector(
'#addRemoveTransitionElms'
);
const ignoreTargetChange: HTMLButtonElement | null = document.querySelector('#ignoreTargetChange');
const setTargetAttr: HTMLSelectElement | null = document.querySelector('#setTargetAttr');
const setFilteredTargetAttr: HTMLSelectElement | null = document.querySelector('#setFilteredTargetAttr');
const setFilteredTargetAttr: HTMLSelectElement | null = document.querySelector(
'#setFilteredTargetAttr'
);
const setContentAttr: HTMLSelectElement | null = document.querySelector('#setContentAttr');
const setFilteredContentAttr: HTMLSelectElement | null = document.querySelector('#setFilteredContentAttr');
const setContentBetweenAttr: HTMLSelectElement | null = document.querySelector('#setContentBetweenAttr');
const setFilteredContentBetweenAttr: HTMLSelectElement | null = document.querySelector('#setFilteredContentBetweenAttr');
const setContentHostElmAttr: HTMLSelectElement | null = document.querySelector('#setContentHostElmAttr');
const setFilteredContentHostElmAttr: HTMLSelectElement | null = document.querySelector('#setFilteredContentHostElmAttr');
const setFilteredContentAttr: HTMLSelectElement | null = document.querySelector(
'#setFilteredContentAttr'
);
const setContentBetweenAttr: HTMLSelectElement | null = document.querySelector(
'#setContentBetweenAttr'
);
const setFilteredContentBetweenAttr: HTMLSelectElement | null = document.querySelector(
'#setFilteredContentBetweenAttr'
);
const setContentHostElmAttr: HTMLSelectElement | null = document.querySelector(
'#setContentHostElmAttr'
);
const setFilteredContentHostElmAttr: HTMLSelectElement | null = document.querySelector(
'#setFilteredContentHostElmAttr'
);
const summaryContent: HTMLElement | null = document.querySelector('#summary-content');
const summaryBetween: HTMLElement | null = document.querySelector('#summary-between');
@@ -66,11 +102,21 @@ const targetDomObserver = createDOMObserver(
document.querySelector('#target')!,
false,
(changedTargetAttrs: string[], styleChanged: boolean) => {
should.ok(Array.isArray(changedTargetAttrs), 'The changedTargetAttrs parameter in a target dom observer must be a array.');
should.equal(typeof styleChanged, 'boolean', 'The styleChanged parameter in a target dom observer must be a boolean.');
should.ok(
Array.isArray(changedTargetAttrs),
'The changedTargetAttrs parameter in a target dom observer must be a array.'
);
should.equal(
typeof styleChanged,
'boolean',
'The styleChanged parameter in a target dom observer must be a boolean.'
);
if (styleChanged && changedTargetAttrs.length === 0) {
should.ok(false, 'Style changing properties must always be inside the changedTargetAttrs array.');
should.ok(
false,
'Style changing properties must always be inside the changedTargetAttrs array.'
);
}
domTargetObserverObservations.push({ changedTargetAttrs, styleChanged });
@@ -106,14 +152,23 @@ const targetDomObserver = createDOMObserver(
}
);
const createContentDomOserver = (eventContentChange: Array<[string?, string?] | null | undefined>) => {
const createContentDomOserver = (
eventContentChange: Array<[string?, string?] | null | undefined>
) => {
return createDOMObserver(
trargetContentElm!,
true,
(contentChangedTroughEvent: boolean) => {
should.equal(typeof contentChangedTroughEvent, 'boolean', 'The contentChanged parameter in a content dom observer must be a boolean.');
should.equal(
typeof contentChangedTroughEvent,
'boolean',
'The contentChanged parameter in a content dom observer must be a boolean.'
);
domContentObserverObservations.push({ contentChange: true, troughEvent: contentChangedTroughEvent });
domContentObserverObservations.push({
contentChange: true,
troughEvent: contentChangedTroughEvent,
});
requestAnimationFrame(() => {
if (contentChangesCountSlot) {
contentChangesCountSlot.textContent = `${domContentObserverObservations.length}`;
@@ -127,7 +182,11 @@ const createContentDomOserver = (eventContentChange: Array<[string?, string?] |
_nestedTargetSelector: hostSelector,
_ignoreContentChange: (mutation, isNestedTarget) => {
const { target, attributeName } = mutation;
return isNestedTarget ? false : attributeName ? liesBetween(target as Element, hostSelector, '.content') : false;
return isNestedTarget
? false
: attributeName
? liesBetween(target as Element, hostSelector, '.content')
: false;
},
_ignoreNestedTargetChange: (target, attrName, oldValue, newValue) => {
if (attrName === 'class' && oldValue && newValue) {
@@ -149,8 +208,10 @@ const createContentDomOserver = (eventContentChange: Array<[string?, string?] |
let contentDomObserver = createContentDomOserver(contentChange);
const getTotalObservations = () => domTargetObserverObservations.length + domContentObserverObservations.length;
const getLast = <T>(arr: T[], indexFromLast = 0): T => arr[arr.length - 1 - indexFromLast] || ({} as T);
const getTotalObservations = () =>
domTargetObserverObservations.length + domContentObserverObservations.length;
const getLast = <T>(arr: T[], indexFromLast = 0): T =>
arr[arr.length - 1 - indexFromLast] || ({} as T);
const changedThrough = <ChangeThrough extends DOMContentObserverResult | DOMTargetObserverResult>(
observationLists?: Array<ChangeThrough[]> | ChangeThrough[]
) => {
@@ -222,7 +283,9 @@ const attrChangeListener = (attrChangeTarget: HTMLElement | null) =>
isClass && target.classList.add('something');
!isClass && target.setAttribute(selectedValue, 'something');
});
const iterateAttrChange = async <ChangeThrough extends DOMContentObserverResult | DOMTargetObserverResult>(
const iterateAttrChange = async <
ChangeThrough extends DOMContentObserverResult | DOMTargetObserverResult
>(
select: HTMLSelectElement | null,
changeThrough?: ChangeThrough[],
checkChange?: (observation: ChangeThrough, selected: string) => any
@@ -248,10 +311,17 @@ const iterateAttrChange = async <ChangeThrough extends DOMContentObserverResult
},
});
};
const addRemoveElementsTest = async (slot: Element | null, changeThrough?: DOMContentObserverResult[] | SeparateChangeThrough) => {
const addRemoveElementsTest = async (
slot: Element | null,
changeThrough?: DOMContentObserverResult[] | SeparateChangeThrough
) => {
if (slot) {
let addChangeThrough: DOMContentObserverResult[] | undefined = changeThrough as DOMContentObserverResult[] | undefined;
let removeChangeThrough: DOMContentObserverResult[] | undefined = changeThrough as DOMContentObserverResult[] | undefined;
let addChangeThrough: DOMContentObserverResult[] | undefined = changeThrough as
| DOMContentObserverResult[]
| undefined;
let removeChangeThrough: DOMContentObserverResult[] | undefined = changeThrough as
| DOMContentObserverResult[]
| undefined;
if (changeThrough && !isArray(changeThrough)) {
addChangeThrough = (changeThrough as SeparateChangeThrough).added;
removeChangeThrough = (changeThrough as SeparateChangeThrough).removed;
@@ -272,7 +342,11 @@ const addRemoveElementsTest = async (slot: Element | null, changeThrough?: DOMCo
if (addChangeThrough) {
const contentChanged = getLast(addChangeThrough);
await waitForOrFailTest(() => {
should.deepEqual(contentChanged, { contentChange: true, troughEvent: false }, 'Adding an content element must result in a content change.');
should.deepEqual(
contentChanged,
{ contentChange: true, troughEvent: false },
'Adding an content element must result in a content change.'
);
});
}
};
@@ -311,7 +385,10 @@ const addRemoveElementsTest = async (slot: Element | null, changeThrough?: DOMCo
await removeElm();
}
};
const triggerSummaryElemet = async (summaryElm: HTMLElement | null, changeThrough?: DOMContentObserverResult[]) => {
const triggerSummaryElemet = async (
summaryElm: HTMLElement | null,
changeThrough?: DOMContentObserverResult[]
) => {
// onyl do if summary is working (IE. exception)
if (summaryElm && (summaryElm.nextElementSibling as HTMLElement)?.offsetHeight === 0) {
const click = async () => {
@@ -370,7 +447,11 @@ const addRemoveImgElmsFn = async () => {
);
const lastContentChanged = getLast(domContentObserverObservations);
should.deepEqual(lastContentChanged, { contentChange: true, troughEvent: true }, 'The images load event must result in a content change.');
should.deepEqual(
lastContentChanged,
{ contentChange: true, troughEvent: true },
'The images load event must result in a content change.'
);
});
};
@@ -424,7 +505,9 @@ const addRemoveImgElmsFn = async () => {
await addMultiple();
// remove load event from image test
const addChanged = async (newEventContentChange: Array<[string?, string?] | null | undefined>) => {
const addChanged = async (
newEventContentChange: Array<[string?, string?] | null | undefined>
) => {
contentDomObserver._destroy();
contentDomObserver = createContentDomOserver(newEventContentChange);
@@ -449,7 +532,16 @@ const addRemoveImgElmsFn = async () => {
contentDomObserver = createContentDomOserver(contentChange);
};
await addChanged([['img', 'something'], ['img', 'something2'], ['img', ''], ['img', undefined], ['', ''], [undefined, undefined], null, undefined]);
await addChanged([
['img', 'something'],
['img', 'something2'],
['img', ''],
['img', undefined],
['', ''],
[undefined, undefined],
null,
undefined,
]);
await addChanged([]);
removeElements(document.querySelectorAll('.img'));
@@ -460,7 +552,11 @@ const addRemoveTransitionElmsFn = async () => {
const startTransition = async (elm: Element, expectTransitionEndContentChange: boolean) => {
await timeout(50); // time for css to apply class a bit later to trigger transition
const { before: beforeTransition, after: afterTransition, compare: compareTransition } = changedThrough(domContentObserverObservations);
const {
before: beforeTransition,
after: afterTransition,
compare: compareTransition,
} = changedThrough(domContentObserverObservations);
beforeTransition();
removeClass(elm, 'resetTransition'); // IE...
addClass(elm, 'active');
@@ -523,7 +619,9 @@ const addRemoveTransitionElmsFn = async () => {
await add(false);
contentDomObserver._destroy();
contentDomObserver = createContentDomOserver(contentChange.concat([['.transition', 'transitionend']]));
contentDomObserver = createContentDomOserver(
contentChange.concat([['.transition', 'transitionend']])
);
await add(true);
};
@@ -557,7 +655,11 @@ const iterateTargetAttrChange = async () => {
true,
'A attribute change on the target element for a DOMTargetObserver must be inside the changedTargetAttrs array.'
);
should.equal(styleChanged, true, 'A style changing attribute on the target element for a DOMTargetObserver must set styleChanged to true.');
should.equal(
styleChanged,
true,
'A style changing attribute on the target element for a DOMTargetObserver must set styleChanged to true.'
);
});
await iterateAttrChange(setFilteredTargetAttr);
};
@@ -605,9 +707,15 @@ setFilteredTargetAttr?.addEventListener('change', attrChangeListener(targetElm))
setContentAttr?.addEventListener('change', attrChangeListener(contentElmAttrChange));
setFilteredContentAttr?.addEventListener('change', attrChangeListener(contentElmAttrChange));
setContentBetweenAttr?.addEventListener('change', attrChangeListener(contentBetweenElmAttrChange));
setFilteredContentBetweenAttr?.addEventListener('change', attrChangeListener(contentBetweenElmAttrChange));
setFilteredContentBetweenAttr?.addEventListener(
'change',
attrChangeListener(contentBetweenElmAttrChange)
);
setContentHostElmAttr?.addEventListener('change', attrChangeListener(contentHostElmAttrChange));
setFilteredContentHostElmAttr?.addEventListener('change', attrChangeListener(contentHostElmAttrChange));
setFilteredContentHostElmAttr?.addEventListener(
'change',
attrChangeListener(contentHostElmAttrChange)
);
const start = async () => {
setTestResult(null);
@@ -0,0 +1,12 @@
// @ts-ignore
import { playwrightRollup, expectSuccess } from '@/playwright/rollup';
import { test } from '@playwright/test';
playwrightRollup();
test.describe('DOMObserver', () => {
test('test', async ({ page }) => {
await page.click('#start');
await expectSuccess(page);
});
});
@@ -2,7 +2,11 @@ import 'styles/overlayscrollbars.scss';
import './index.scss';
import './handleEnvironment';
import should from 'should';
import { generateClassChangeSelectCallback, iterateSelect, selectOption } from '@/testing-browser/Select';
import {
generateClassChangeSelectCallback,
iterateSelect,
selectOption,
} from '@/testing-browser/Select';
import { setTestResult, waitForOrFailTest } from '@/testing-browser/TestResult';
import { timeout } from '@/testing-browser/timeout';
import { hasDimensions, offsetSize, WH, style } from 'support';
@@ -15,8 +19,12 @@ const contentBox = (elm: HTMLElement | null): WH<number> => {
if (elm) {
const computedStyle = window.getComputedStyle(elm);
return {
w: elm.clientWidth - (parseFloat(computedStyle.paddingLeft) + parseFloat(computedStyle.paddingRight)),
h: elm.clientHeight - (parseFloat(computedStyle.paddingTop) + parseFloat(computedStyle.paddingBottom)),
w:
elm.clientWidth -
(parseFloat(computedStyle.paddingLeft) + parseFloat(computedStyle.paddingRight)),
h:
elm.clientHeight -
(parseFloat(computedStyle.paddingTop) + parseFloat(computedStyle.paddingBottom)),
};
}
@@ -37,7 +45,7 @@ const preInitChildren = targetElm?.children.length;
const sizeObserver = createSizeObserver(
targetElm as HTMLElement,
({ _directionIsRTLCache, _sizeChanged, _appear }) => {
({ _directionIsRTLCache, _sizeChanged }) => {
if (_sizeChanged) {
sizeIterations += 1;
}
@@ -45,6 +53,7 @@ const sizeObserver = createSizeObserver(
if (_directionIsRTLCache) {
directionIterations += 1;
}
requestAnimationFrame(() => {
if (resizesSlot) {
resizesSlot.textContent = (directionIterations + sizeIterations).toString();
@@ -83,13 +92,22 @@ const iterate = async (select: HTMLSelectElement | null, afterEach?: () => any)
currBoxSizing,
};
},
async check({ currSizeIterations, currDirectionIterations, currOffsetSize, currContentSize, currDir, currBoxSizing }) {
async check({
currSizeIterations,
currDirectionIterations,
currOffsetSize,
currContentSize,
currDir,
currBoxSizing,
}) {
const newOffsetSize = offsetSize(targetElm as HTMLElement);
const newContentSize = contentBox(targetElm as HTMLElement);
const newDir = style(targetElm as HTMLElement, 'direction');
const newBoxSizing = style(targetElm as HTMLElement, 'box-sizing');
const offsetSizeChanged = currOffsetSize.w !== newOffsetSize.w || currOffsetSize.h !== newOffsetSize.h;
const contentSizeChanged = currContentSize.w !== newContentSize.w || currContentSize.h !== newContentSize.h;
const offsetSizeChanged =
currOffsetSize.w !== newOffsetSize.w || currOffsetSize.h !== newOffsetSize.h;
const contentSizeChanged =
currContentSize.w !== newContentSize.w || currContentSize.h !== newContentSize.h;
const dirChanged = currDir !== newDir;
const boxSizingChanged = currBoxSizing !== newBoxSizing;
const dimensions = hasDimensions(targetElm as HTMLElement);
@@ -113,20 +131,36 @@ const iterate = async (select: HTMLSelectElement | null, afterEach?: () => any)
if (dirChanged) {
await waitForOrFailTest(() => {
const expectedCacheValue = newDir === 'rtl';
should.equal(directionIterations, currDirectionIterations + 1, 'Direction change was detected correctly.');
should.equal(sizeObserver._getCurrentCacheValues()._directionIsRTL._value, expectedCacheValue, 'Direction cache value is correct.');
should.equal(
directionIterations,
currDirectionIterations + 1,
'Direction change was detected correctly.'
);
should.equal(
sizeObserver._getCurrentCacheValues()._directionIsRTL[0],
expectedCacheValue,
'Direction cache value is correct.'
);
});
}
if (boxSizingChanged) {
await waitForOrFailTest(() => {
should.equal(sizeIterations, currSizeIterations + 1, 'BoxSizing change was detected correctly.');
should.equal(
sizeIterations,
currSizeIterations + 1,
'BoxSizing change was detected correctly.'
);
});
}
if (dimensions && (offsetSizeChanged || contentSizeChanged)) {
await waitForOrFailTest(() => {
should.equal(sizeIterations, currSizeIterations + 1, 'Size change was detected correctly.');
should.equal(
sizeIterations,
currSizeIterations + 1,
'Size change was detected correctly.'
);
});
}
@@ -229,7 +263,11 @@ const start = async () => {
await cleanBoxSizingChange();
sizeObserver._destroy();
should.equal(targetElm?.children.length, preInitChildren, 'Destruction removes all generated elements.');
should.equal(
targetElm?.children.length,
preInitChildren,
'Destruction removes all generated elements.'
);
setTestResult(true);
};
@@ -0,0 +1,19 @@
// @ts-ignore
import { playwrightRollup, expectSuccess } from '@/playwright/rollup';
import { test } from '@playwright/test';
playwrightRollup();
test.describe('SizeObserver', () => {
test('with ResizeOserver', async ({ page }) => {
await page.click('#start');
await expectSuccess(page);
});
test('with ResizeOserver polyfill', async ({ page }) => {
await page.click('#roPolyfill');
await page.waitForTimeout(500);
await page.click('#start');
await expectSuccess(page);
});
});
@@ -2,7 +2,11 @@ import 'styles/overlayscrollbars.scss';
import './index.scss';
import './handleEnvironment';
import should from 'should';
import { generateClassChangeSelectCallback, iterateSelect, selectOption } from '@/testing-browser/Select';
import {
generateClassChangeSelectCallback,
iterateSelect,
selectOption,
} from '@/testing-browser/Select';
import { timeout } from '@/testing-browser/timeout';
import { setTestResult, waitForOrFailTest } from '@/testing-browser/TestResult';
import { offsetSize } from 'support';
@@ -22,9 +26,10 @@ const changesSlot: HTMLButtonElement | null = document.querySelector('#changes')
const preInitChildren = targetElm?.children.length;
const trinsicObserver = createTrinsicObserver(targetElm as HTMLElement, (heightIntrinsicCache) => {
if (heightIntrinsicCache._changed) {
const [currentHeightIntrinsic, currentHeightIntrinsicChanged] = heightIntrinsicCache;
if (currentHeightIntrinsicChanged) {
heightIterations += 1;
heightIntrinsic = heightIntrinsicCache._value;
heightIntrinsic = currentHeightIntrinsic;
}
requestAnimationFrame(() => {
if (changesSlot) {
@@ -65,10 +70,14 @@ const iterate = async (select: HTMLSelectElement | null, afterEach?: () => any)
await waitForOrFailTest(() => {
if (trinsicHeightChanged) {
should.equal(heightIterations, currHeightIterations + 1, 'Height intrinsic change has been detected correctly.');
should.equal(
heightIterations,
currHeightIterations + 1,
'Height intrinsic change has been detected correctly.'
);
}
should.equal(
trinsicObserver._getCurrentCacheValues()._heightIntrinsic._value,
trinsicObserver._getCurrentCacheValues()._heightIntrinsic[0],
newHeightIntrinsic,
'Height intrinsic cache value is correct.'
);
@@ -97,7 +106,11 @@ const changeWhileHidden = async () => {
selectOption(displaySelect as HTMLSelectElement, 'displayBlock');
await waitForOrFailTest(() => {
should.equal(heightIntrinsic, false, 'Trinsic sizing changes while hidden from intrinsic to extrinsic.');
should.equal(
heightIntrinsic,
false,
'Trinsic sizing changes while hidden from intrinsic to extrinsic.'
);
});
};
@@ -111,7 +124,11 @@ const changeWhileHidden = async () => {
selectOption(displaySelect as HTMLSelectElement, 'displayBlock');
await waitForOrFailTest(() => {
should.equal(heightIntrinsic, true, 'Trinsic sizing changes while hidden from extrinsic to intrinsic.');
should.equal(
heightIntrinsic,
true,
'Trinsic sizing changes while hidden from extrinsic to intrinsic.'
);
});
};
@@ -133,7 +150,11 @@ const start = async () => {
await changeWhileHidden();
trinsicObserver._destroy();
should.equal(targetElm?.children.length, preInitChildren, 'After destruction all generated elements are removed.');
should.equal(
targetElm?.children.length,
preInitChildren,
'After destruction all generated elements are removed.'
);
setTestResult(true);
};
@@ -0,0 +1,28 @@
// @ts-ignore
import { playwrightRollup, expectSuccess } from '@/playwright/rollup';
import { test } from '@playwright/test';
playwrightRollup();
test.describe('TrinsicObserver', () => {
test('with IntersectionObserver', async ({ page }) => {
await page.click('#start');
await expectSuccess(page);
});
test('with ResizeObserver', async ({ page }) => {
await page.click('#ioPolyfill');
await page.waitForTimeout(500);
await page.click('#start');
await expectSuccess(page);
});
test('with ResizeObserver polyfill', async ({ page }) => {
await page.click('#ioPolyfill');
await page.waitForTimeout(500);
await page.click('#roPolyfill');
await page.waitForTimeout(500);
await page.click('#start');
await expectSuccess(page);
});
});

Some files were not shown because too many files have changed in this diff Show More