diff --git a/cypress/component/Truncate.cy.tsx b/cypress/component/Truncate.cy.tsx
new file mode 100644
index 0000000000..70c21ede44
--- /dev/null
+++ b/cypress/component/Truncate.cy.tsx
@@ -0,0 +1,303 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 - present Instructure, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+import React from 'react'
+import { expect } from 'chai'
+
+import '../support/component'
+import { within } from '@instructure/ui-utils'
+import truncate from '../../packages/ui-truncate-text/src/TruncateText/utils/truncate'
+
+
+describe('truncate', () => {
+ const defaultText = 'Hello world! This is a long string that should truncate'
+ const baseStyle = {
+ fontSize: '16px',
+ fontFamily: 'Arial',
+ fontWeight: 'normal',
+ fontStyle: 'normal',
+ letterSpacing: 'normal'
+ }
+
+ it('should truncate text when no options are given', async () => {
+ cy.mount(
+
+ {defaultText}
+
+ )
+
+ cy.get('#stage').then(($stage) => {
+ truncate($stage[0])
+ const text = $stage.text()
+
+ expect(text.indexOf('truncate')).to.equal(-1)
+ expect(text.indexOf('\u2026')).to.not.equal(-1)
+ })
+
+ cy.get('#stage').should('have.text', 'Hello world! This is a long…')
+ })
+
+ it('should truncate in the middle of a string', async () => {
+ cy.mount(
+
+ {defaultText}
+
+ )
+
+ cy.get('#stage').then(($stage) => {
+ truncate($stage[0], { position: 'middle' })
+ const text = $stage.text()
+
+ expect(text.indexOf('long')).to.equal(-1)
+ expect(text.indexOf('Hello')).to.not.equal(-1)
+ expect(text.indexOf('truncate')).to.not.equal(-1)
+ expect(text.indexOf('\u2026')).to.not.equal(-1)
+ })
+
+ cy.get('#stage').should('have.text', 'Hello world! …ould truncate')
+ })
+
+ it('should truncate at words', async () => {
+ cy.mount(
+
+ {defaultText}
+
+ )
+
+ cy.get('#stage').then(($stage) => {
+ truncate($stage[0], { truncate: 'word' })
+ const text = $stage.text()
+
+ expect(text.indexOf('string')).to.equal(-1)
+ expect(text.indexOf('st')).to.equal(-1)
+ expect(text.indexOf('long')).to.not.equal(-1)
+ })
+
+ cy.get('#stage').should('have.text', 'Hello world! This is a long …')
+ })
+
+ it('should allow custom ellipsis', async () => {
+ cy.mount(
+
+ {defaultText}
+
+ )
+
+ cy.get('#stage').then(($stage) => {
+ truncate($stage[0], { ellipsis: '(...)' })
+ const text = $stage.text()
+
+ expect(text!.slice(-5)).to.equal('(...)')
+ })
+
+ cy.get('#stage').should('have.text', 'Hello world! This is a lon(...)')
+ })
+
+ it('should preserve node structure', async () => {
+ cy.mount(
+
+
+ Hello world! This is a long string that{' '}
+ should truncate
+
+
+ )
+
+ cy.get('#stage').then(($stage) => {
+ truncate($stage[0])
+
+ cy.wrap($stage[0].childNodes[1].nodeType).should('equal', 1)
+ cy.wrap($stage[0].childNodes[2].nodeType).should('equal', 3)
+ cy.wrap($stage[0].children.length).should('equal', 2)
+ cy.wrap($stage[0].className).should('equal', 'testClass')
+ cy.wrap($stage[0].tagName).should('equal', 'P')
+ })
+
+ cy.get('strong').should('exist')
+ cy.get('#stage').should('have.text', 'Hello world! This is a lon…')
+ })
+
+ it('should preserve attributes on nodes', async () => {
+ cy.mount(
+
+
+ This is a{' '}
+
+ text link
+ {' '}
+ with classes and an href.
+
+
+ )
+
+ cy.get('#stage').then(($stage) => {
+ truncate($stage[0])
+ })
+
+ cy.get('#link')
+ .should('have.attr', 'href', 'http://google.com')
+ .and('have.attr', 'class', 'tester')
+ .and('have.attr', 'id', 'link')
+
+ cy.get('#link').then(($link) => {
+ const attributesLength = $link[0].attributes.length
+ expect(attributesLength).to.equal(3)
+ })
+ })
+
+ it('should calculate max width properly', async () => {
+ cy.mount(
+
+ )
+
+ cy.get('#stage').then(($stage) => {
+ const result = truncate($stage[0])
+ const maxWidth = result!.constraints.width
+
+ cy.get('#textContainer').then(($textContainer) => {
+ const actualMax = $textContainer[0].getBoundingClientRect().width
+
+ expect(within(maxWidth, actualMax, 1)).to.equal(true)
+ })
+ })
+ })
+
+ it('should calculate `maxLines: auto` correctly', async () => {
+ cy.mount(
+
+ {defaultText}
+
+ )
+
+ cy.get('#stage').then(($stage) => {
+ const result = truncate($stage[0], { maxLines: 'auto' })
+ const text = $stage.text()
+
+ cy.wrap(text).should('not.equal', defaultText)
+ cy.wrap(text.length).should('not.equal', 1)
+ cy.wrap(result!.constraints.lines).should('equal', 4)
+ })
+ })
+
+ it('should calculate height correctly when `maxLines` is not `auto`', async () => {
+ cy.mount(
+
+ {defaultText}
+
+ )
+
+ cy.get('#stage').then(($stage) => {
+ const result = truncate($stage[0])
+ const text = $stage.text()
+
+ cy.wrap(text.length).should('not.equal', 1)
+
+ cy.wrap(text).should('not.equal', defaultText)
+ cy.wrap(result!.constraints.height).should('equal', 22.4)
+ })
+ })
+
+ it('should escape node content', async () => {
+ cy.spy(console, 'log').as('consoleLogSpy')
+ const content = '">
'
+
+ cy.mount(
+
+ {content}
+
+ )
+
+ cy.get('#stage').then(($stage) => {
+ truncate($stage[0])
+
+ cy.wrap($stage.text()).should('equal', content)
+ cy.get('@consoleLogSpy').should('not.have.been.calledWith', 'hello world')
+ })
+ })
+
+ it('should truncate when visually hidden', async () => {
+ cy.mount(
+
+ {defaultText}
+
+ )
+
+ cy.get('#stage').then(($stage) => {
+ truncate($stage[0])
+ const text = $stage.text()
+
+ expect(text.indexOf('truncate')).to.equal(-1)
+ expect(text.indexOf('\u2026')).to.not.equal(-1)
+ })
+
+ cy.get('#stage-wrapper').should('have.css', 'opacity', '0')
+ cy.get('#stage').should('have.text', 'Hello world! This is a long…')
+ })
+
+ it('should account for font size styles', async () => {
+ cy.mount(
+
+ {defaultText}
+
+ )
+
+ cy.get('#stage').then(($stageInitial) => {
+ truncate($stageInitial[0])
+ })
+
+ cy.get('#stage').should('have.text', 'Hello world! This is a long…')
+
+ // Update font size
+ cy.get('#stage').invoke('css', { ...baseStyle, width: '200px', fontSize: '24px' })
+
+ cy.get('#stage').then(($stageUpdated) => {
+ truncate($stageUpdated[0])
+ })
+
+ cy.get('#stage').should('have.text', 'Hello world! This…')
+ })
+})
diff --git a/cypress/component/TruncateText.cy.tsx b/cypress/component/TruncateText.cy.tsx
new file mode 100644
index 0000000000..3a7ecfb8df
--- /dev/null
+++ b/cypress/component/TruncateText.cy.tsx
@@ -0,0 +1,206 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 - present Instructure, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+import React from 'react'
+import { TruncateText, Text } from '@instructure/ui'
+import { expect } from 'chai'
+import '../support/component'
+
+
+describe('', () => {
+ const defaultText = 'Hello world! This is a long string that should truncate'
+ const baseStyle = {
+ fontSize: '16px',
+ fontFamily: 'Arial',
+ fontWeight: 'normal',
+ fontStyle: 'normal',
+ letterSpacing: 'normal'
+ }
+
+ it('should truncate text', async () => {
+ cy.mount(
+
+ {defaultText}
+
+ )
+
+ cy.get('[class$="-truncateText"]')
+ .invoke('text')
+ .should('not.contain', 'truncate')
+ .and('contain', '\u2026')
+ })
+
+ it('should recalculate when parent width changes', async () => {
+ cy.mount(
+
+ {defaultText}
+
+ )
+
+ cy.get('[class$="-truncateText"]').invoke('text').as('text1')
+
+ cy.get('[data-testid="container"]').invoke('css', { ...baseStyle, width: '100px'})
+
+ cy.get('[class$="-truncateText"]').invoke('text').as('text2')
+
+ cy.get('@text2').then((text2) => {
+ cy.get('@text1').should('not.equal', text2)
+ })
+
+ cy.get('[data-testid="container"]').invoke('css', { ...baseStyle, width: '400px'})
+
+
+ cy.get('[class$="-truncateText"]').invoke('text').as('text3')
+
+ cy.get('@text3').then((text3) => {
+ cy.get('@text2').should('not.equal', text3)
+ })
+ })
+
+ it('should preserve node structure', async () => {
+ cy.mount(
+
+
+
+ Hello world! This is a long string that{' '}
+ should truncate
+
+
+
+ )
+
+ cy.get('p.testClass')
+ .as('paragraph')
+ .should('exist')
+ .and('have.class', 'testClass')
+ .within(() => {
+ cy.get('strong').should('exist')
+ cy.get('em').should('exist')
+ })
+
+ cy.get('@paragraph').then((paragraph) => {
+ expect(paragraph[0].children.length).to.equal(3)
+ })
+ })
+
+ it('should recalculate if props change', async () => {
+ cy.mount(
+
+ {defaultText}
+
+ )
+
+ cy.get('[class$="-truncateText"]')
+ .invoke('text')
+ .then((textBeforeUpdate) => {
+ // Set props: position, ellipsis
+ cy.mount(
+
+
+ {defaultText}
+
+
+ )
+
+ cy.get('[class$="-truncateText"]')
+ .invoke('text')
+ .should('not.equal', textBeforeUpdate)
+ })
+ })
+
+ it('should re-render with new children if children change', async () => {
+ cy.mount(
+
+ {defaultText}
+
+ )
+
+ cy.get('[class$="-truncateText"]')
+ .invoke('text')
+ .then((textBeforeUpdate) => {
+ // Set child
+ cy.mount(
+
+
+ This is a different string of text
+
+
+ )
+
+ cy.get('[class$="-truncateText"]')
+ .invoke('text')
+ .should('not.equal', textBeforeUpdate)
+ })
+ })
+
+ it('should call onUpdate when text changes', async () => {
+ const onUpdate = cy.stub()
+
+ cy.mount(
+
+ {defaultText}
+
+ )
+ cy.wrap(onUpdate).should('not.have.been.called')
+
+ // Set container width
+ cy.get('[data-testid="container"]').invoke('css', { ...baseStyle, width: '100px'})
+ cy.wrap(onUpdate).should('have.been.calledWith', true)
+
+ // Set container width
+ cy.get('[data-testid="container"]').invoke('css', { ...baseStyle, width: '800px'})
+ cy.wrap(onUpdate).should('have.been.calledWith', false)
+ })
+
+ it('should render text at any size with no lineHeight set', async () => {
+ cy.mount(
+
+
+
+ xsmall
+
+
+ small
+
+
+ medium
+
+
+ large
+
+
+ xlarge
+
+
+ xxlarge
+
+
+
+ )
+
+ cy.get('[data-testid="container"]').should(
+ 'contain',
+ 'xsmallsmallmediumlargexlargexxlarge'
+ )
+ })
+})
diff --git a/package-lock.json b/package-lock.json
index 440cd7343a..813c62ac4b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -44116,12 +44116,16 @@
"prop-types": "^15.8.1"
},
"devDependencies": {
+ "@instructure/ui-axe-check": "10.11.0",
"@instructure/ui-babel-preset": "10.11.0",
"@instructure/ui-color-utils": "10.11.0",
"@instructure/ui-test-utils": "10.11.0",
"@instructure/ui-text": "10.11.0",
"@instructure/ui-themes": "10.11.0",
- "@types/escape-html": "^1.0.4"
+ "@testing-library/jest-dom": "^6.6.3",
+ "@testing-library/react": "^16.0.1",
+ "@types/escape-html": "^1.0.4",
+ "vitest": "^2.1.8"
},
"peerDependencies": {
"react": ">=16.14 <=18"
diff --git a/packages/ui-truncate-text/package.json b/packages/ui-truncate-text/package.json
index 41d3924eec..0fd9c08968 100644
--- a/packages/ui-truncate-text/package.json
+++ b/packages/ui-truncate-text/package.json
@@ -36,12 +36,16 @@
"prop-types": "^15.8.1"
},
"devDependencies": {
+ "@instructure/ui-axe-check": "10.11.0",
"@instructure/ui-babel-preset": "10.11.0",
"@instructure/ui-color-utils": "10.11.0",
"@instructure/ui-test-utils": "10.11.0",
"@instructure/ui-text": "10.11.0",
"@instructure/ui-themes": "10.11.0",
- "@types/escape-html": "^1.0.4"
+ "@types/escape-html": "^1.0.4",
+ "@testing-library/jest-dom": "^6.6.3",
+ "@testing-library/react": "^16.0.1",
+ "vitest": "^2.1.8"
},
"peerDependencies": {
"react": ">=16.14 <=18"
diff --git a/packages/ui-truncate-text/src/TruncateText/__new-tests__/TruncateText.test.tsx b/packages/ui-truncate-text/src/TruncateText/__new-tests__/TruncateText.test.tsx
new file mode 100644
index 0000000000..92aaf197b3
--- /dev/null
+++ b/packages/ui-truncate-text/src/TruncateText/__new-tests__/TruncateText.test.tsx
@@ -0,0 +1,99 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 - present Instructure, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import React from 'react'
+import { render, waitFor } from '@testing-library/react'
+import { vi, expect } from 'vitest'
+import type { MockInstance } from 'vitest'
+
+import '@testing-library/jest-dom'
+import { runAxeCheck } from '@instructure/ui-axe-check'
+import { TruncateText } from '../index'
+
+const defaultText = 'Hello world! This is a long string that should truncate'
+
+describe('', () => {
+ let consoleErrorMock: ReturnType
+
+ beforeEach(() => {
+ // Mocking console to prevent test output pollution and expect for messages
+ consoleErrorMock = vi
+ .spyOn(console, 'error')
+ .mockImplementation(() => {}) as MockInstance
+ })
+
+ afterEach(() => {
+ consoleErrorMock.mockRestore()
+ })
+
+ it('should warn if children prop receives too deep of a node tree', async () => {
+ render(
+
+
+ Hello world!{' '}
+
+ This is a
+ {' '}
+ long string that should truncate
+
+
+ )
+
+ const expectedErrorMessage =
+ 'Some children are too deep in the node tree and will not render.'
+
+ await waitFor(() => {
+ expect(consoleErrorMock).toHaveBeenCalledWith(
+ expect.stringContaining(expectedErrorMessage),
+ expect.any(String)
+ )
+ })
+ })
+
+ it('should handle the empty string as a child', async () => {
+ let error = false
+
+ try {
+ const { rerender } = render({''})
+
+ rerender({'hello world'})
+ rerender({''})
+ } catch (_e) {
+ error = true
+ }
+
+ expect(error).toBe(false)
+ })
+
+ it('should meet a11y standards', async () => {
+ const { container } = render(
+
+ {defaultText}
+
+ )
+ const axeCheck = await runAxeCheck(container)
+
+ expect(axeCheck).toBe(true)
+ })
+})
diff --git a/packages/ui-truncate-text/src/TruncateText/__tests__/TruncateText.test.tsx b/packages/ui-truncate-text/src/TruncateText/__tests__/TruncateText.test.tsx
deleted file mode 100644
index 1fd731ed70..0000000000
--- a/packages/ui-truncate-text/src/TruncateText/__tests__/TruncateText.test.tsx
+++ /dev/null
@@ -1,254 +0,0 @@
-/*
- * The MIT License (MIT)
- *
- * Copyright (c) 2015 - present Instructure, Inc.
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-import React from 'react'
-import { expect, mount, stub, wait, within } from '@instructure/ui-test-utils'
-import { Text } from '@instructure/ui-text'
-
-import { TruncateText } from '../index'
-
-describe('', async () => {
- const defaultText = 'Hello world! This is a long string that should truncate'
-
- it('should truncate text', async () => {
- const subject = await mount(
-
- {defaultText}
-
,
- { strictMode: false }
- )
-
- const renderedContent = within(subject.getDOMNode())
- const text = renderedContent.getTextContent()!
-
- expect(text.indexOf('truncate')).to.equal(-1)
- expect(text.indexOf('\u2026')).to.not.equal(-1)
- })
-
- it('should recalculate when parent width changes', async () => {
- let container: HTMLElement
-
- const subject = await mount(
- {
- container = el
- }}
- >
- {defaultText}
-
- )
-
- const renderedContent = within(subject.getDOMNode())
- const text1 = renderedContent.getTextContent()
-
- container!.style.width = '100px'
-
- let text2: string | null
- await wait(() => {
- text2 = renderedContent.getTextContent()
- expect(text1).to.not.equal(text2)
- })
-
- container!.style.width = '400px'
-
- await wait(() => {
- expect(renderedContent.getTextContent()).to.not.equal(text2)
- })
- })
-
- it('should preserve node structure', async () => {
- const subject = await mount(
-
-
-
- Hello world! This is a long string that{' '}
- should truncate
-
-
-
,
- { strictMode: false }
- )
-
- const renderedContent = within(subject.getDOMNode())
- const paragraph = await renderedContent.find('p')
-
- expect(await paragraph.find('strong')).to.exist()
- expect(await paragraph.find('em')).to.exist()
-
- expect(paragraph.hasClass('testClass')).to.be.true()
- expect(paragraph.getDOMNode().children.length).to.equal(3)
- })
-
- it('should recalculate if props change', async () => {
- const subject = await mount(
-
- {defaultText}
-
- )
-
- const renderedContent = within(subject.getDOMNode())
- const text = renderedContent.getTextContent()
-
- await subject.setProps({
- children: (
-
- {defaultText}
-
- )
- })
-
- await wait(() => {
- expect(renderedContent.getTextContent()).to.not.equal(text)
- })
- })
-
- it('should re-render with new children if children change', async () => {
- const subject = await mount(
-
- {defaultText}
-
- )
-
- const renderedContent = within(subject.getDOMNode())
- const text = renderedContent.getTextContent()
-
- await subject.setProps({
- children: This is a different string of text
- })
-
- await wait(() => {
- expect(renderedContent.getTextContent()).to.not.equal(text)
- })
- })
-
- it('should call onUpdate when text changes', async () => {
- const onUpdate = stub()
-
- let container: HTMLElement
- await mount(
- {
- container = el
- }}
- >
- {defaultText}
-
- )
-
- expect(onUpdate).to.not.have.been.called()
-
- container!.style.width = '100px'
- await wait(() => {
- expect(onUpdate).to.have.been.calledWith(true)
- })
-
- container!.style.width = '800px'
- await wait(() => {
- expect(onUpdate).to.have.been.calledWith(false)
- })
- })
-
- it('should warn if children prop receives too deep of a node tree', async () => {
- const consoleError = stub(console, 'error')
- const warning =
- 'Some children are too deep in the node tree and will not render.'
- await mount(
-
-
- Hello world!{' '}
-
- This is a
- {' '}
- long string that should truncate
-
-
- )
- await wait(() => {
- expect(consoleError).to.have.been.calledWithMatch(warning)
- })
- })
-
- it('should render text at any size with no lineHeight set', async () => {
- let stage: HTMLSpanElement | null
- await mount(
-
- {
- stage = el
- }}
- >
-
- xsmall
-
-
- small
-
-
- medium
-
-
- large
-
-
- xlarge
-
-
- xxlarge
-
-
-
- )
-
- expect(stage!.textContent).to.equal('xsmallsmallmediumlargexlargexxlarge')
- })
-
- it('should handle the empty string as a child', async () => {
- let error = false
-
- try {
- const subject = await mount({''})
-
- await subject.setProps({ children: 'hello world' })
- await subject.setProps({ children: '' })
- } catch (e) {
- error = true
- }
-
- expect(error).to.be.false()
- })
-
- it('should meet a11y standards', async () => {
- const subject = await mount(
-
- {defaultText}
-
- )
- const renderedContent = within(subject.getDOMNode())
- expect(await renderedContent.accessible()).to.be.true()
- })
-})
diff --git a/packages/ui-truncate-text/src/TruncateText/utils/__tests__/cleanData.test.ts b/packages/ui-truncate-text/src/TruncateText/utils/__new-tests__/cleanData.test.tsx
similarity index 97%
rename from packages/ui-truncate-text/src/TruncateText/utils/__tests__/cleanData.test.ts
rename to packages/ui-truncate-text/src/TruncateText/utils/__new-tests__/cleanData.test.tsx
index 91c1043a5c..7a08b54798 100644
--- a/packages/ui-truncate-text/src/TruncateText/utils/__tests__/cleanData.test.ts
+++ b/packages/ui-truncate-text/src/TruncateText/utils/__new-tests__/cleanData.test.tsx
@@ -21,12 +21,12 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
-
-import { expect } from '@instructure/ui-test-utils'
+import { expect } from 'chai'
+import '@testing-library/jest-dom'
import cleanData from '../cleanData'
import { CleanDataOptions } from '../../props'
-describe('cleanData', async () => {
+describe('cleanData', () => {
it('should remove spaces from the end of character data', async () => {
const data = [['T', 'e', 's', 't', ' ', '...']]
const options: Required = {
diff --git a/packages/ui-truncate-text/src/TruncateText/utils/__tests__/cleanString.test.ts b/packages/ui-truncate-text/src/TruncateText/utils/__new-tests__/cleanString.test.tsx
similarity index 95%
rename from packages/ui-truncate-text/src/TruncateText/utils/__tests__/cleanString.test.ts
rename to packages/ui-truncate-text/src/TruncateText/utils/__new-tests__/cleanString.test.tsx
index bc57a7af52..13fbba801e 100644
--- a/packages/ui-truncate-text/src/TruncateText/utils/__tests__/cleanString.test.ts
+++ b/packages/ui-truncate-text/src/TruncateText/utils/__new-tests__/cleanString.test.tsx
@@ -22,10 +22,11 @@
* SOFTWARE.
*/
-import { expect } from '@instructure/ui-test-utils'
+import { expect } from 'chai'
+import '@testing-library/jest-dom'
import cleanString from '../cleanString'
-describe('cleanSring', async () => {
+describe('cleanSring', () => {
it('should remove spaces from start and end of string', async () => {
const string = ' Hello world '
diff --git a/packages/ui-truncate-text/src/TruncateText/utils/__new-tests__/measureText.test.tsx b/packages/ui-truncate-text/src/TruncateText/utils/__new-tests__/measureText.test.tsx
new file mode 100644
index 0000000000..74f667c9b0
--- /dev/null
+++ b/packages/ui-truncate-text/src/TruncateText/utils/__new-tests__/measureText.test.tsx
@@ -0,0 +1,173 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 - present Instructure, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import React from 'react'
+import { render, screen } from '@testing-library/react'
+import { vi, expect } from 'vitest'
+
+import '@testing-library/jest-dom'
+import measureText from '../measureText'
+
+const baseStyle = {
+ fontSize: '16px',
+ fontFamily: 'Arial',
+ fontWeight: 'normal',
+ fontStyle: 'normal',
+ letterSpacing: 'normal'
+}
+
+const getNodes = (root: Element) =>
+ Array.from(root.childNodes).filter(
+ (node) =>
+ node.nodeType === Node.ELEMENT_NODE || node.nodeType === Node.TEXT_NODE
+ )
+
+describe('measureText', () => {
+ beforeEach(() => {
+ vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockImplementation(
+ () => {
+ return {
+ font: '',
+ measureText: vi.fn((text: string) => {
+ return { width: text.length * 10 }
+ })
+ } as unknown as CanvasRenderingContext2D
+ }
+ )
+ })
+
+ afterEach(() => {
+ vi.restoreAllMocks()
+ })
+
+ it('should calculate the width of a single text node correctly', () => {
+ const node = document.createElement('div')
+ Object.assign(node.style, baseStyle)
+ node.textContent = 'Hello'
+
+ const width = measureText([node])
+
+ expect(width).toBe(50) // 5 characters * 10 (mocked width per character)
+ })
+
+ it('should return zero for empty nodes', () => {
+ const node = document.createElement('div')
+ Object.assign(node.style, baseStyle)
+ node.textContent = ''
+
+ const width = measureText([node])
+
+ expect(width).toBe(0)
+ })
+
+ it('should calculate text width correctly', () => {
+ render(
+
+ Lorem ipsum DOLOR SIT AMET.
+
+ )
+ const stage = screen.getByTestId('stage')
+ const nodes = getNodes(stage)
+
+ const width = measureText(nodes, stage)
+
+ expect(width).toBe(270)
+ })
+
+ it('should account for different nodes', async () => {
+ const { rerender } = render(
+
+ Lorem ipsum DOLOR SIT AMET.
+
+ )
+ const stage = screen.getByTestId('stage')
+ const nodes = getNodes(stage)
+
+ const width = measureText(nodes, stage)
+
+ // Set child
+ rerender(
+
+ Lorem ipsum DOLOR SIT AMET.
+
+ )
+ const stage2 = screen.getByTestId('stage')
+ const nodes2 = getNodes(stage2)
+
+ const width2 = measureText(nodes2, stage2)
+
+ expect(width).toEqual(width2)
+ })
+
+ it('should call measureText on a properly configured canvas 2d context', () => {
+ const mockedStyle = {
+ fontSize: '20px',
+ fontWeight: 'bold',
+ fontStyle: 'italic',
+ fontFamily: 'Arial'
+ }
+
+ render(
+
+ Lorem ipsum
+
+ )
+ const stage = screen.getByTestId('stage')
+ const nodes = getNodes(stage)
+
+ measureText(nodes, stage)
+
+ // The width calculation depends on the modified canvas context
+ const getContextSpy = HTMLCanvasElement.prototype.getContext as any
+ const context = getContextSpy.mock.results[0].value
+
+ expect(context.measureText).toHaveBeenCalledTimes(1)
+ expect(context.font).toBe('bold italic 20px Arial')
+ })
+
+ it('should account for letter spacing styles', async () => {
+ const { rerender } = render(
+
+ Lorem ipsum
+
+ )
+ const stage = screen.getByTestId('stage')
+ const nodes = getNodes(stage)
+ const width = measureText(nodes, stage)
+
+ expect(width).toBe(110) // default mocked width (text.length * 10)
+
+ // Set letterSpacing
+ rerender(
+
+ Lorem ipsum
+
+ )
+ const stage2 = screen.getByTestId('stage2')
+ const nodes2 = getNodes(stage2)
+ const width2 = measureText(nodes2, stage2)
+
+ expect(width2).toBe(165) // default mocked width + letterOffset (text.length * letterSpacing)
+ })
+})
diff --git a/packages/ui-truncate-text/src/TruncateText/utils/__tests__/measureText.test.tsx b/packages/ui-truncate-text/src/TruncateText/utils/__tests__/measureText.test.tsx
deleted file mode 100644
index dfa0a74aeb..0000000000
--- a/packages/ui-truncate-text/src/TruncateText/utils/__tests__/measureText.test.tsx
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
- * The MIT License (MIT)
- *
- * Copyright (c) 2015 - present Instructure, Inc.
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-import React from 'react'
-import { expect, mount } from '@instructure/ui-test-utils'
-
-import measureText from '../measureText'
-
-describe('measureText', async () => {
- const getNodes = (root: Element) =>
- Array.from(root.childNodes).filter(
- (node) =>
- node.nodeType === Node.ELEMENT_NODE || node.nodeType === Node.TEXT_NODE
- )
-
- it('should calculate width', async () => {
- let stage: HTMLElement
- await mount(
- {
- stage = el
- }}
- >
- Lorem ipsum DOLOR SIT AMET.
-
- )
-
- const nodes = getNodes(stage!)
-
- const width = measureText(nodes, stage!)
- expect(width).to.not.equal(0)
- })
-
- it('should account for different nodes', async () => {
- let stage: HTMLElement
- const subject = await mount(
- {
- stage = el
- }}
- >
- Lorem ipsum DOLOR SIT AMET.
-
- )
-
- const nodes = getNodes(stage!)
-
- const width = measureText(nodes, stage!)
-
- await subject.setProps({
- children: 'Lorem ipsum DOLOR SIT AMET.'
- })
-
- const nodes2 = getNodes(stage!)
- const width2 = measureText(nodes2, stage!)
-
- expect(Math.floor(width)).to.equal(Math.floor(width2))
- })
-
- it('should account for font size styles', async () => {
- let stage: HTMLElement
-
- const subject = await mount(
- {
- stage = el
- }}
- >
- Lorem ipsum DOLOR SIT AMET.
-
- )
-
- const nodes = getNodes(stage!)
- const width = measureText(nodes, stage!)
-
- await subject.setProps({
- style: { fontSize: '24px' }
- })
-
- const width2 = measureText(nodes, stage!)
- expect(width).to.not.equal(width2)
- })
-
- it('should account for letter spacing styles', async () => {
- let stage: HTMLElement
-
- const subject = await mount(
- {
- stage = el
- }}
- >
- Lorem ipsum DOLOR SIT AMET.
-
- )
-
- const nodes = getNodes(stage!)
- const width = measureText(nodes, stage!)
-
- await subject.setProps({
- style: { letterSpacing: '5px' }
- })
-
- const width2 = measureText(nodes, stage!)
- expect(width).to.not.equal(width2)
- })
-})
diff --git a/packages/ui-truncate-text/src/TruncateText/utils/__tests__/truncate.test.tsx b/packages/ui-truncate-text/src/TruncateText/utils/__tests__/truncate.test.tsx
deleted file mode 100644
index a8a59b6de4..0000000000
--- a/packages/ui-truncate-text/src/TruncateText/utils/__tests__/truncate.test.tsx
+++ /dev/null
@@ -1,296 +0,0 @@
-/*
- * The MIT License (MIT)
- *
- * Copyright (c) 2015 - present Instructure, Inc.
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-import React from 'react'
-import { expect, mount, spy } from '@instructure/ui-test-utils'
-import { within } from '@instructure/ui-utils'
-
-import truncate from '../truncate'
-
-describe('truncate', () => {
- const defaultText = 'Hello world! This is a long string that should truncate'
-
- it('should truncate text when no options are given', async () => {
- let stage: Element | null
- await mount(
-
- {
- stage = el
- }}
- >
- {defaultText}
-
-
- )
- truncate(stage!)
- const text = stage!.textContent!
-
- expect(text.indexOf('truncate')).to.equal(-1)
- expect(text.indexOf('\u2026')).to.not.equal(-1)
- })
-
- it('should truncate in the middle of a string', async () => {
- let stage: Element | null
- await mount(
-
- {
- stage = el
- }}
- >
- {defaultText}
-
-
- )
-
- truncate(stage!, { position: 'middle' })
- const text = stage!.textContent!
-
- expect(text.indexOf('long')).to.equal(-1)
- expect(text.indexOf('Hello')).to.not.equal(-1)
- expect(text.indexOf('truncate')).to.not.equal(-1)
- expect(text.indexOf('\u2026')).to.not.equal(-1)
- })
-
- it('should truncate at words', async () => {
- let stage: Element | null
- await mount(
-
- {
- stage = el
- }}
- >
- {defaultText}
-
-
- )
-
- truncate(stage!, { truncate: 'word' })
-
- const text = stage!.textContent!
-
- expect(text.indexOf('string')).to.equal(-1)
- expect(text.indexOf('st')).to.equal(-1)
- expect(text.indexOf('long')).to.not.equal(-1)
- })
-
- it('should allow custom ellipsis', async () => {
- let stage: Element | null
- await mount(
-
- {
- stage = el
- }}
- >
- {defaultText}
-
-
- )
-
- truncate(stage!, { ellipsis: '(...)' })
- const text = stage!.textContent
-
- expect(text!.slice(-5)).to.equal('(...)')
- })
-
- it('should preserve node structure', async () => {
- let stage: Element | null
- await mount(
-
-
{
- stage = el
- }}
- className="testClass"
- >
- Hello world! This is a long string that{' '}
- should truncate
-
-
- )
-
- truncate(stage!)
-
- expect(stage!.childNodes[1].nodeType).to.equal(1)
- expect(stage!.childNodes[2].nodeType).to.equal(3)
- expect(stage!.children.length).to.equal(2)
- expect(stage!.nodeName).to.equal('P')
- expect(stage!.className).to.equal('testClass')
- })
-
- it('should preserve attributes on nodes', async () => {
- let stage: Element | null
- let link: HTMLAnchorElement | null
- await mount(
-
- )
-
- truncate(stage!)
- const attr: Record = link!.attributes!
- expect(attr.length).to.equal(2)
- expect(attr.href).to.exist()
- expect(attr.class).to.exist()
- })
-
- it('should calculate max width properly', async () => {
- let textContainer: Element | null
- let stage: Element | null
- await mount(
-
-
-
{
- textContainer = el
- }}
- >
- {defaultText}
-
-
-
{
- stage = el
- }}
- >
- {defaultText}
-
-
-
-
- )
-
- const result = truncate(stage!)
-
- const maxWidth = result!.constraints.width
- const actualMax = textContainer!.getBoundingClientRect().width
-
- expect(within(maxWidth, actualMax, 1)).to.be.true()
- })
-
- it('should calculate `maxLines: auto` correctly', async () => {
- let stage: Element | null
- await mount(
-
- {
- stage = el
- }}
- >
- {defaultText}
-
-
- )
-
- const result = truncate(stage!, { maxLines: 'auto' })
- const text = stage!.textContent
-
- expect(text).to.not.equal({ defaultText })
- expect(text!.length).to.not.equal(1)
- expect(result!.constraints.lines).to.equal(4)
- })
-
- it('should calculate height correctly when `maxLines` is not `auto`', async () => {
- let stage: Element | null
- await mount(
-
- {
- stage = el
- }}
- >
- {defaultText}
-
-
- )
-
- const result = truncate(stage!)!
- const text = stage!.textContent!
-
- expect(text.length).to.not.equal(1)
- expect(result.constraints.height).to.equal(22.4)
- })
-
- it('should escape node content', async () => {
- const log = spy(console, 'log')
- const content = '">
'
-
- let stage: Element | null
- await mount(
-
- {
- stage = el
- }}
- >
- {content}
-
-
- )
-
- truncate(stage!)
-
- expect(stage!.textContent).to.equal(content)
- expect(log).to.not.have.been.calledWith('hello world')
- })
-
- it('should truncate when visually hidden', async () => {
- let stage: Element | null
- await mount(
-
- {
- stage = el
- }}
- >
- {defaultText}
-
-
- )
- truncate(stage!)
- const text = stage!.textContent!
-
- expect(text.indexOf('truncate')).to.equal(-1)
- expect(text.indexOf('\u2026')).to.not.equal(-1)
- })
-})
diff --git a/packages/ui-truncate-text/tsconfig.build.json b/packages/ui-truncate-text/tsconfig.build.json
index c8261fee48..b81fbe7792 100644
--- a/packages/ui-truncate-text/tsconfig.build.json
+++ b/packages/ui-truncate-text/tsconfig.build.json
@@ -7,6 +7,7 @@
},
"include": ["./src"],
"references": [
+ { "path": "../ui-axe-check/tsconfig.build.json" },
{ "path": "../console/tsconfig.build.json" },
{ "path": "../debounce/tsconfig.build.json" },
{ "path": "../emotion/tsconfig.build.json" },