diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..316706d --- /dev/null +++ b/.babelrc @@ -0,0 +1,8 @@ +{ + "presets": ["@babel/preset-env"], + "env": { + "test": { + "presets": [["@babel/preset-env"]] + } + } +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.gitignore b/.gitignore index 3c3629e..7a1537b 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ +.idea node_modules diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..28ece47 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "endOfLine": "lf", + "semi": false, + "singleQuote": false, + "tabWidth": 2, + "trailingComma": "es5", + bracketSpacing: true, + jsxBracketSameLine: true +} diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..d012f71 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,30 @@ +language: node_js +cache: npm +notifications: + email: false +node_js: + - 10.0.0 + - 12 + - node +install: + - npm install +script: + - npm run lint + - npx run test +branches: + only: + - master + - beta + +jobs: + include: + # Define the release stage that runs semantic-release + - stage: release + node_js: lts/* + # Advanced: optionally overwrite your default `script` step to skip the tests + # script: skip + deploy: + provider: script + skip_cleanup: true + script: + - npx semantic-release diff --git a/jest-preprocess.js b/jest-preprocess.js new file mode 100644 index 0000000..cf53a90 --- /dev/null +++ b/jest-preprocess.js @@ -0,0 +1,6 @@ +const babelOptions = { + presets: ['@babel/preset-env'], +}; + +// eslint-disable-next-line import/no-extraneous-dependencies +module.exports = require('babel-jest').createTransformer(babelOptions); diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..35a3df4 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,8 @@ +module.exports = { + clearMocks: true, + coverageDirectory: "coverage", + testMatch: ['/tests/*.js'], + transform: { + '^.+\\.js?$': `/jest-preprocess.js`, + }, +}; diff --git a/package.json b/package.json index de876b7..5ab7cd3 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,18 @@ { "name": "twig-testing-library", - "version": "0.0.0-semantically-released", + "version": "0.0.0-development", "description": "Simple and complete Twig testing utilities that encourage good testing practices.", "main": "dist/index.js", "scripts": { - "test": "jest" + "test": "jest", + "format": "prettier --write \"{test,src}/**/*.js\"", + "lint": "prettier --check \"{test,src}/**/*.js\"", + "coverage": "jest --collect-coverage --collectCoverageFrom=\"src/**/*.js\"", + "semantic-release": "semantic-release" }, "repository": { "type": "git", - "url": "git+https://github.com/larowlan/twig-testing-library.git" + "url": "https://github.com/larowlan/twig-testing-library.git" }, "keywords": [ "testing", @@ -30,6 +34,15 @@ "homepage": "https://github.com/larowlan/twig-testing-library#readme", "dependencies": { "@babel/runtime": "^7.9.2", - "@testing-library/dom": "^7.2.0" + "@testing-library/dom": "^7.2.0", + "drupal-attribute": "^1.0.2", + "twig": "^1.15.0" + }, + "devDependencies": { + "@babel/polyfill": "^7.8.7", + "@babel/preset-env": "^7.9.5", + "jest": "^25.3.0", + "prettier": "^2.0.4", + "semantic-release": "^17.0.4" } } diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..f3aaf20 --- /dev/null +++ b/src/index.js @@ -0,0 +1,94 @@ +import { getQueriesForElement, prettyDOM } from "@testing-library/dom" +import Twig from "twig" +import fs from "fs" +import "@babel/polyfill" +import DrupalAttribute from "drupal-attribute" + +const mountedContainers = new Set() + +if (typeof afterEach === "function") { + afterEach(cleanup) +} + +async function render(twigFile, context = {}, namespaces = {}) { + const baseElement = document.body + const container = baseElement.appendChild(document.createElement("div")) + + // Add it to the mounted containers to cleanup. + mountedContainers.add(container) + + container.innerHTML = await loadTemplate(twigFile, context, namespaces) + + return { + container, + baseElement, + debug: (el = baseElement, maxLength, options) => + Array.isArray(el) + ? // eslint-disable-next-line no-console + el.forEach((e) => console.log(prettyDOM(e, maxLength, options))) + : // eslint-disable-next-line no-console, + console.log(prettyDOM(el, maxLength, options)), + ...getQueriesForElement(baseElement), + } +} + +const loadTemplate = async (file, context = {}, namespaces) => { + Twig.registryReset = () => { + Twig.Templates.registry = {} + } + + Twig.cache(false) + Twig.extendFunction("create_attribute", (value) => new DrupalAttribute(value)) + Twig.twigAsync = (options) => { + return new Promise((resolve, reject) => { + options.load = resolve + options.error = reject + options.async = true + options.autoescape = false + options.namespaces = namespaces + + if (options.data || options.ref) { + try { + resolve(Twig.twig(options)) + } catch (error) { + reject(error) + } + } else { + fs.readFile(options.path, "utf8", (err, data) => { + if (err) { + reject(new Error(`Unable to find template file ${options.path}`)) + return + } + options.load = (template) => { + template.rawMarkup = data + resolve(template) + } + Twig.twig(options) + }) + } + }) + } + return Twig.twigAsync({ + path: file, + }).then((template) => { + context.attributes = new DrupalAttribute() + return template.render(context) + }) +} + +function cleanup() { + mountedContainers.forEach(cleanupContainer) +} + +function cleanupContainer(container) { + if (container.parentNode === document.body) { + document.body.removeChild(container) + } + mountedContainers.delete(container) +} + +// just re-export everything from dom-testing-library +export * from "@testing-library/dom" +export { render, cleanup } + +/* eslint func-name-matching:0 */ diff --git a/tests/__snapshots__/index.js.snap b/tests/__snapshots__/index.js.snap new file mode 100644 index 0000000..df4840c --- /dev/null +++ b/tests/__snapshots__/index.js.snap @@ -0,0 +1,192 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Test library by testing an accordion Can be initially rendered closed: First expand 1`] = ` +
+ + + + Accordion title + + + +
+ + +

+ Cras quis nulla commodo, aliquam lectus sed, blandit augue. Cras ullamcorper bibendum bibendum. Duis tincidunt urna non pretium porta. Nam condimentum vitae ligula vel ornare. Phasellus. + +

+ + +
+ + +
+`; + +exports[`Test library by testing an accordion Can be initially rendered closed: Initial render 1`] = ` +
+ + + + Accordion title + + + +
+ + +

+ Cras quis nulla commodo, aliquam lectus sed, blandit augue. Cras ullamcorper bibendum bibendum. Duis tincidunt urna non pretium porta. Nam condimentum vitae ligula vel ornare. Phasellus. + +

+ + +
+ + +
+`; + +exports[`Test library by testing an accordion Can be initially rendered closed: Re-collapse 1`] = ` +
+ + + + + +
+ + +

+ Cras quis nulla commodo, aliquam lectus sed, blandit augue. Cras ullamcorper bibendum bibendum. Duis tincidunt urna non pretium porta. Nam condimentum vitae ligula vel ornare. Phasellus. + +

+ + +
+ + +
+`; + +exports[`Test library by testing an accordion Can be initially rendered open: First collapse 1`] = ` +
+ + + + + +
+ + +

+ Cras quis nulla commodo, aliquam lectus sed, blandit augue. Cras ullamcorper bibendum bibendum. Duis tincidunt urna non pretium porta. Nam condimentum vitae ligula vel ornare. Phasellus. + +

+ + +
+ + +
+`; + +exports[`Test library by testing an accordion Can be initially rendered open: Initial render 1`] = ` +
+ + + + Accordion title + + + +
+ + +

+ Cras quis nulla commodo, aliquam lectus sed, blandit augue. Cras ullamcorper bibendum bibendum. Duis tincidunt urna non pretium porta. Nam condimentum vitae ligula vel ornare. Phasellus. + +

+ + +
+ + +
+`; + +exports[`Test library by testing an accordion Can be initially rendered open: Re-open 1`] = ` +
+ + + + Accordion title + + + +
+ + +

+ Cras quis nulla commodo, aliquam lectus sed, blandit augue. Cras ullamcorper bibendum bibendum. Duis tincidunt urna non pretium porta. Nam condimentum vitae ligula vel ornare. Phasellus. + +

+ + +
+ + +
+`; diff --git a/tests/fixtures/accordion.js b/tests/fixtures/accordion.js new file mode 100644 index 0000000..521b469 --- /dev/null +++ b/tests/fixtures/accordion.js @@ -0,0 +1,46 @@ +/** + * Accordion + * @file Accordion handler. + */ + +class Accordion { + constructor(obj) { + this.accordion = obj; + this.summary = obj.querySelector('.accordion__title'); + } + + init() { + const open = this.accordion.hasAttribute('open'); + if (open) { + this.accordion.classList.add('accordion--open'); + } + this.summary.addEventListener('focus', () => { + this.handleFocus(); + }); + this.summary.addEventListener('blur', () => { + this.handleBlur(); + }); + this.summary.addEventListener('click', () => { + this.handleClick(); + }); + } + + handleFocus() { + // Focus class for styling. + this.accordion.classList.add('has-focus'); + } + + handleBlur() { + // Focus class for styling. + this.accordion.classList.remove('has-focus'); + } + + handleClick() { + const open = this.accordion.classList.contains('accordion--open'); + this.summary.setAttribute('aria-expanded', !open); + this.summary.setAttribute('aria-pressed', !open); + this.accordion.classList.toggle('accordion--open'); + } +} + +export default { Accordion }; diff --git a/tests/fixtures/accordion.twig b/tests/fixtures/accordion.twig new file mode 100644 index 0000000..1dcaedf --- /dev/null +++ b/tests/fixtures/accordion.twig @@ -0,0 +1,8 @@ +
+ {{ title|default('Accordion title') }} +
+ {% block accordion_content %} +

{% include "@twig-testing-library-tests/lorem-ipsum.twig" %}

+ {% endblock %} +
+
diff --git a/tests/fixtures/lorem-ipsum.twig b/tests/fixtures/lorem-ipsum.twig new file mode 100644 index 0000000..a0619bb --- /dev/null +++ b/tests/fixtures/lorem-ipsum.twig @@ -0,0 +1 @@ +Cras quis nulla commodo, aliquam lectus sed, blandit augue. Cras ullamcorper bibendum bibendum. Duis tincidunt urna non pretium porta. Nam condimentum vitae ligula vel ornare. Phasellus. diff --git a/tests/index.js b/tests/index.js new file mode 100644 index 0000000..f4541f6 --- /dev/null +++ b/tests/index.js @@ -0,0 +1,57 @@ +/* eslint no-new: 0 */ +import Accordion from './fixtures/accordion'; +import {render, fireEvent} from "../src"; + +describe('Test library by testing an accordion', () => { + it('Can be initially rendered open', async () => { + //expect.assertions(8); + const { container, getByText, debug } = await render('./tests/fixtures/accordion.twig', { + title: 'Accordion title', + open: true, + }, { + 'twig-testing-library-tests': './tests/fixtures/' + }); + const accordionElement = container.querySelector('.accordion'); + const summaryElement = accordionElement.querySelector('summary'); + const accordion = new Accordion.Accordion(accordionElement); + accordion.init(); + expect(accordionElement).toMatchSnapshot('Initial render'); + expect(accordionElement.classList.contains('accordion--open')).toBe(true); + fireEvent.click(getByText('Accordion title')); + expect(accordionElement).toMatchSnapshot('First collapse'); + expect(accordionElement.classList.contains('accordion--open')).toBe(false); + expect(summaryElement.getAttribute('aria-expanded')).toEqual('false'); + expect(summaryElement.getAttribute('aria-pressed')).toEqual('false'); + fireEvent.click(getByText('Accordion title')); + expect(accordionElement).toMatchSnapshot('Re-open'); + expect(accordionElement.classList.contains('accordion--open')).toBe(true); + expect(summaryElement.getAttribute('aria-expanded')).toEqual('true'); + expect(summaryElement.getAttribute('aria-pressed')).toEqual('true'); + }); + + it('Can be initially rendered closed', async () => { + //expect.assertions(8); + const { container, getByText, debug } = await render('./tests/fixtures/accordion.twig', { + title: 'Accordion title', + open: false, + }, { + 'twig-testing-library-tests': './tests/fixtures/' + }); + const accordionElement = container.querySelector('.accordion'); + const summaryElement = accordionElement.querySelector('summary'); + const accordion = new Accordion.Accordion(accordionElement); + accordion.init(); + expect(accordionElement).toMatchSnapshot('Initial render'); + expect(accordionElement.classList.contains('accordion--open')).toBe(false); + fireEvent.click(getByText('Accordion title')); + expect(accordionElement).toMatchSnapshot('First expand'); + expect(accordionElement.classList.contains('accordion--open')).toBe(true); + expect(summaryElement.getAttribute('aria-expanded')).toEqual('true'); + expect(summaryElement.getAttribute('aria-pressed')).toEqual('true'); + fireEvent.click(getByText('Accordion title')); + expect(accordionElement).toMatchSnapshot('Re-collapse'); + expect(accordionElement.classList.contains('accordion--open')).toBe(false); + expect(summaryElement.getAttribute('aria-expanded')).toEqual('false'); + expect(summaryElement.getAttribute('aria-pressed')).toEqual('false'); + }); +});