Add tests
parent
0964dbfe4a
commit
bac7e2e564
File diff suppressed because it is too large
Load Diff
|
@ -18,7 +18,9 @@
|
|||
"clean": "rimraf bin",
|
||||
"hashbang": "node scripts/hashbang.mjs",
|
||||
"create-notices-file": "node scripts/createNoticesFile.mjs",
|
||||
"prepublishOnly": "npm run build"
|
||||
"prepublishOnly": "npm run build",
|
||||
"test": "vitest",
|
||||
"test:prepare": "node tests/prepare.js"
|
||||
},
|
||||
"bin": {
|
||||
"tailwindcss-language-server": "./bin/tailwindcss-language-server"
|
||||
|
@ -65,7 +67,9 @@
|
|||
"stack-trace": "0.0.10",
|
||||
"tailwindcss": "3.3.0",
|
||||
"typescript": "4.6.4",
|
||||
"vitest": "0.34.2",
|
||||
"vscode-css-languageservice": "5.4.1",
|
||||
"vscode-jsonrpc": "8.1.0",
|
||||
"vscode-languageserver": "8.0.2",
|
||||
"vscode-languageserver-textdocument": "1.0.7",
|
||||
"vscode-uri": "3.0.2"
|
||||
|
|
|
@ -1200,10 +1200,13 @@ async function createProjectService(
|
|||
URI.file(path.resolve(path.dirname(URI.parse(document.uri).fsPath), linkPath)).toString()
|
||||
)
|
||||
},
|
||||
provideDiagnostics: debounce((document: TextDocument) => {
|
||||
provideDiagnostics: debounce(
|
||||
(document: TextDocument) => {
|
||||
if (!state.enabled) return
|
||||
provideDiagnostics(state, document)
|
||||
}, 500),
|
||||
},
|
||||
params.initializationOptions?.testMode ? 0 : 500
|
||||
),
|
||||
provideDiagnosticsForce: (document: TextDocument) => {
|
||||
if (!state.enabled) return
|
||||
provideDiagnostics(state, document)
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
import { test, expect } from 'vitest'
|
||||
import * as fs from 'node:fs/promises'
|
||||
import { withFixture } from '../common'
|
||||
|
||||
withFixture('basic', (c) => {
|
||||
function testFixture(fixture) {
|
||||
test(fixture, async () => {
|
||||
fixture = await fs.readFile(`tests/code-actions/${fixture}.json`, 'utf8')
|
||||
|
||||
let { code, expected, language = 'html' } = JSON.parse(fixture)
|
||||
|
||||
let promise = new Promise((resolve) => {
|
||||
c.onNotification('textDocument/publishDiagnostics', ({ diagnostics }) => {
|
||||
resolve(diagnostics)
|
||||
})
|
||||
})
|
||||
|
||||
let textDocument = await c.openDocument({ text: code, lang: language })
|
||||
let diagnostics = await promise
|
||||
|
||||
let res = await c.sendRequest('textDocument/codeAction', {
|
||||
textDocument,
|
||||
context: {
|
||||
diagnostics,
|
||||
},
|
||||
})
|
||||
// console.log(JSON.stringify(res))
|
||||
|
||||
expected = JSON.parse(JSON.stringify(expected).replaceAll('{{URI}}', textDocument.uri))
|
||||
|
||||
expect(res).toEqual(expected)
|
||||
})
|
||||
}
|
||||
|
||||
testFixture('conflict')
|
||||
testFixture('invalid-theme')
|
||||
testFixture('invalid-screen')
|
||||
})
|
|
@ -0,0 +1,36 @@
|
|||
import { test, expect } from 'vitest'
|
||||
import { withFixture } from '../common'
|
||||
import * as fs from 'node:fs/promises'
|
||||
|
||||
withFixture('v2-jit', (c) => {
|
||||
function testFixture(fixture) {
|
||||
test(fixture, async () => {
|
||||
fixture = await fs.readFile(`tests/code-actions/${fixture}.json`, 'utf8')
|
||||
|
||||
let { code, expected, language = 'html' } = JSON.parse(fixture)
|
||||
|
||||
let promise = new Promise((resolve) => {
|
||||
c.onNotification('textDocument/publishDiagnostics', ({ diagnostics }) => {
|
||||
resolve(diagnostics)
|
||||
})
|
||||
})
|
||||
|
||||
let textDocument = await c.openDocument({ text: code, lang: language })
|
||||
let diagnostics = await promise
|
||||
|
||||
let res = await c.sendRequest('textDocument/codeAction', {
|
||||
textDocument,
|
||||
context: {
|
||||
diagnostics,
|
||||
},
|
||||
})
|
||||
// console.log(JSON.stringify(res))
|
||||
|
||||
expected = JSON.parse(JSON.stringify(expected).replaceAll('{{URI}}', textDocument.uri))
|
||||
|
||||
expect(res).toEqual(expected)
|
||||
})
|
||||
}
|
||||
|
||||
testFixture('variant-order')
|
||||
})
|
|
@ -0,0 +1,39 @@
|
|||
import { test, expect } from 'vitest'
|
||||
import { withFixture } from '../common'
|
||||
import * as fs from 'node:fs/promises'
|
||||
|
||||
withFixture('v2', (c) => {
|
||||
function testFixture(fixture) {
|
||||
test(fixture, async () => {
|
||||
fixture = await fs.readFile(`tests/code-actions/${fixture}.json`, 'utf8')
|
||||
|
||||
let { code, expected, language = 'html' } = JSON.parse(fixture)
|
||||
|
||||
let promise = new Promise((resolve) => {
|
||||
c.onNotification('textDocument/publishDiagnostics', ({ diagnostics }) => {
|
||||
resolve(diagnostics)
|
||||
})
|
||||
})
|
||||
|
||||
let textDocument = await c.openDocument({ text: code, lang: language })
|
||||
let diagnostics = await promise
|
||||
|
||||
let res = await c.sendRequest('textDocument/codeAction', {
|
||||
textDocument,
|
||||
context: {
|
||||
diagnostics,
|
||||
},
|
||||
})
|
||||
// console.log(JSON.stringify(res))
|
||||
|
||||
expected = JSON.parse(JSON.stringify(expected).replaceAll('{{URI}}', textDocument.uri))
|
||||
|
||||
expect(res).toEqual(expected)
|
||||
})
|
||||
}
|
||||
|
||||
// testFixture('conflict')
|
||||
testFixture('invalid-theme')
|
||||
testFixture('invalid-screen')
|
||||
testFixture('invalid-variant')
|
||||
})
|
|
@ -0,0 +1,161 @@
|
|||
{
|
||||
"code": "<div class=\"lowercase uppercase\">",
|
||||
"expected": [
|
||||
{
|
||||
"title": "Delete 'uppercase'",
|
||||
"kind": "quickfix",
|
||||
"diagnostics": [
|
||||
{
|
||||
"code": "cssConflict",
|
||||
"className": {
|
||||
"className": "lowercase",
|
||||
"classList": {
|
||||
"classList": "lowercase uppercase",
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 12 },
|
||||
"end": { "line": 0, "character": 31 }
|
||||
}
|
||||
},
|
||||
"relativeRange": {
|
||||
"start": { "line": 0, "character": 0 },
|
||||
"end": { "line": 0, "character": 9 }
|
||||
},
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 12 },
|
||||
"end": { "line": 0, "character": 21 }
|
||||
}
|
||||
},
|
||||
"otherClassNames": [
|
||||
{
|
||||
"className": "uppercase",
|
||||
"classList": {
|
||||
"classList": "lowercase uppercase",
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 12 },
|
||||
"end": { "line": 0, "character": 31 }
|
||||
}
|
||||
},
|
||||
"relativeRange": {
|
||||
"start": { "line": 0, "character": 10 },
|
||||
"end": { "line": 0, "character": 19 }
|
||||
},
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 22 },
|
||||
"end": { "line": 0, "character": 31 }
|
||||
}
|
||||
}
|
||||
],
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 12 },
|
||||
"end": { "line": 0, "character": 21 }
|
||||
},
|
||||
"severity": 2,
|
||||
"message": "'lowercase' applies the same CSS properties as 'uppercase'.",
|
||||
"relatedInformation": [
|
||||
{
|
||||
"message": "uppercase",
|
||||
"location": {
|
||||
"uri": "{{URI}}",
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 22 },
|
||||
"end": { "line": 0, "character": 31 }
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"edit": {
|
||||
"changes": {
|
||||
"{{URI}}": [
|
||||
{
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 12 },
|
||||
"end": { "line": 0, "character": 31 }
|
||||
},
|
||||
"newText": "lowercase"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Delete 'lowercase'",
|
||||
"kind": "quickfix",
|
||||
"diagnostics": [
|
||||
{
|
||||
"code": "cssConflict",
|
||||
"className": {
|
||||
"className": "uppercase",
|
||||
"classList": {
|
||||
"classList": "lowercase uppercase",
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 12 },
|
||||
"end": { "line": 0, "character": 31 }
|
||||
}
|
||||
},
|
||||
"relativeRange": {
|
||||
"start": { "line": 0, "character": 10 },
|
||||
"end": { "line": 0, "character": 19 }
|
||||
},
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 22 },
|
||||
"end": { "line": 0, "character": 31 }
|
||||
}
|
||||
},
|
||||
"otherClassNames": [
|
||||
{
|
||||
"className": "lowercase",
|
||||
"classList": {
|
||||
"classList": "lowercase uppercase",
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 12 },
|
||||
"end": { "line": 0, "character": 31 }
|
||||
}
|
||||
},
|
||||
"relativeRange": {
|
||||
"start": { "line": 0, "character": 0 },
|
||||
"end": { "line": 0, "character": 9 }
|
||||
},
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 12 },
|
||||
"end": { "line": 0, "character": 21 }
|
||||
}
|
||||
}
|
||||
],
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 22 },
|
||||
"end": { "line": 0, "character": 31 }
|
||||
},
|
||||
"severity": 2,
|
||||
"message": "'uppercase' applies the same CSS properties as 'lowercase'.",
|
||||
"relatedInformation": [
|
||||
{
|
||||
"message": "lowercase",
|
||||
"location": {
|
||||
"uri": "{{URI}}",
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 12 },
|
||||
"end": { "line": 0, "character": 21 }
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"edit": {
|
||||
"changes": {
|
||||
"{{URI}}": [
|
||||
{
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 12 },
|
||||
"end": { "line": 0, "character": 31 }
|
||||
},
|
||||
"newText": "uppercase"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"code": "@screen small",
|
||||
"language": "css",
|
||||
"expected": [
|
||||
{
|
||||
"title": "Replace with 'sm'",
|
||||
"kind": "quickfix",
|
||||
"diagnostics": [
|
||||
{
|
||||
"code": "invalidScreen",
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 8 },
|
||||
"end": { "line": 0, "character": 13 }
|
||||
},
|
||||
"severity": 1,
|
||||
"message": "The screen 'small' does not exist in your theme config. Did you mean 'sm'?",
|
||||
"suggestions": ["sm"]
|
||||
}
|
||||
],
|
||||
"edit": {
|
||||
"changes": {
|
||||
"{{URI}}": [
|
||||
{
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 8 },
|
||||
"end": { "line": 0, "character": 13 }
|
||||
},
|
||||
"newText": "sm"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"code": ".test { color: theme(colors.red.901) }",
|
||||
"language": "css",
|
||||
"expected": [
|
||||
{
|
||||
"title": "Replace with 'colors.red.900'",
|
||||
"kind": "quickfix",
|
||||
"diagnostics": [
|
||||
{
|
||||
"code": "invalidConfigPath",
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 21 },
|
||||
"end": { "line": 0, "character": 35 }
|
||||
},
|
||||
"severity": 1,
|
||||
"message": "'colors.red.901' does not exist in your theme config. Did you mean 'colors.red.900'?",
|
||||
"suggestions": ["colors.red.900"]
|
||||
}
|
||||
],
|
||||
"edit": {
|
||||
"changes": {
|
||||
"{{URI}}": [
|
||||
{
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 21 },
|
||||
"end": { "line": 0, "character": 35 }
|
||||
},
|
||||
"newText": "colors.red.900"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"code": "@variants hoover",
|
||||
"language": "css",
|
||||
"expected": [
|
||||
{
|
||||
"title": "Replace with 'hover'",
|
||||
"kind": "quickfix",
|
||||
"diagnostics": [
|
||||
{
|
||||
"code": "invalidVariant",
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 10 },
|
||||
"end": { "line": 0, "character": 16 }
|
||||
},
|
||||
"severity": 1,
|
||||
"message": "The variant 'hoover' does not exist. Did you mean 'hover'?",
|
||||
"suggestions": ["hover"]
|
||||
}
|
||||
],
|
||||
"edit": {
|
||||
"changes": {
|
||||
"{{URI}}": [
|
||||
{
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 10 },
|
||||
"end": { "line": 0, "character": 16 }
|
||||
},
|
||||
"newText": "hover"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"code": "<div class=\"hover:focus:uppercase\">",
|
||||
"expected": [
|
||||
{
|
||||
"title": "Replace with 'focus:hover:uppercase'",
|
||||
"kind": "quickfix",
|
||||
"diagnostics": [
|
||||
{
|
||||
"code": "recommendedVariantOrder",
|
||||
"suggestions": ["focus:hover:uppercase"],
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 12 },
|
||||
"end": { "line": 0, "character": 33 }
|
||||
},
|
||||
"severity": 2,
|
||||
"message": "Variants are not in the recommended order, which may cause unexpected CSS output."
|
||||
}
|
||||
],
|
||||
"edit": {
|
||||
"changes": {
|
||||
"{{URI}}": [
|
||||
{
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 12 },
|
||||
"end": { "line": 0, "character": 33 }
|
||||
},
|
||||
"newText": "focus:hover:uppercase"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
import { test, expect } from 'vitest'
|
||||
import { withFixture } from '../common'
|
||||
|
||||
withFixture('basic', (c) => {
|
||||
async function testColors(name, { text, expected }) {
|
||||
test.concurrent(name, async () => {
|
||||
let textDocument = await c.openDocument({ text })
|
||||
let res = await c.sendRequest('textDocument/documentColor', {
|
||||
textDocument,
|
||||
})
|
||||
|
||||
expect(res).toEqual(expected)
|
||||
})
|
||||
}
|
||||
|
||||
testColors('simple', {
|
||||
text: '<div class="bg-red-500">',
|
||||
expected: [
|
||||
{
|
||||
range: { start: { line: 0, character: 12 }, end: { line: 0, character: 22 } },
|
||||
color: {
|
||||
red: 0.9372549019607843,
|
||||
green: 0.26666666666666666,
|
||||
blue: 0.26666666666666666,
|
||||
alpha: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
testColors('opacity modifier', {
|
||||
text: '<div class="bg-red-500/20">',
|
||||
expected: [
|
||||
{
|
||||
range: { start: { line: 0, character: 12 }, end: { line: 0, character: 25 } },
|
||||
color: {
|
||||
red: 0.9372549019607843,
|
||||
green: 0.26666666666666666,
|
||||
blue: 0.26666666666666666,
|
||||
alpha: 0.2,
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
testColors('arbitrary value', {
|
||||
text: '<div class="bg-[red]">',
|
||||
expected: [
|
||||
{
|
||||
range: { start: { line: 0, character: 12 }, end: { line: 0, character: 20 } },
|
||||
color: {
|
||||
red: 1,
|
||||
green: 0,
|
||||
blue: 0,
|
||||
alpha: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
testColors('arbitrary value and opacity modifier', {
|
||||
text: '<div class="bg-[red]/[0.33]">',
|
||||
expected: [
|
||||
{
|
||||
range: { start: { line: 0, character: 12 }, end: { line: 0, character: 27 } },
|
||||
color: {
|
||||
red: 1,
|
||||
green: 0,
|
||||
blue: 0,
|
||||
alpha: 0.33,
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
|
@ -0,0 +1,108 @@
|
|||
import { test, expect } from 'vitest'
|
||||
import { withFixture } from '../common'
|
||||
|
||||
withFixture('basic', (c) => {
|
||||
test.concurrent('theme color', async () => {
|
||||
let textDocument = await c.openDocument({ text: '<div class="bg-red-500">' })
|
||||
let res = await c.sendRequest('textDocument/colorPresentation', {
|
||||
color: { red: 1, green: 0, blue: 0, alpha: 1 },
|
||||
textDocument,
|
||||
range: {
|
||||
start: { line: 0, character: 12 },
|
||||
end: { line: 0, character: 22 },
|
||||
},
|
||||
})
|
||||
|
||||
expect(res).toEqual([])
|
||||
})
|
||||
|
||||
test.concurrent('arbitrary named color', async () => {
|
||||
let textDocument = await c.openDocument({ text: '<div class="bg-[red]">' })
|
||||
let res = await c.sendRequest('textDocument/colorPresentation', {
|
||||
color: { red: 1, green: 0, blue: 0, alpha: 1 },
|
||||
textDocument,
|
||||
range: {
|
||||
start: { line: 0, character: 12 },
|
||||
end: { line: 0, character: 20 },
|
||||
},
|
||||
})
|
||||
|
||||
expect(res).toEqual([
|
||||
{ label: 'bg-[#ff0000]' },
|
||||
{ label: 'bg-[rgb(255,0,0)]' },
|
||||
{ label: 'bg-[hsl(0,100%,50%)]' },
|
||||
])
|
||||
})
|
||||
|
||||
test.concurrent('arbitrary short hex color', async () => {
|
||||
let textDocument = await c.openDocument({ text: '<div class="bg-[#f00]">' })
|
||||
let res = await c.sendRequest('textDocument/colorPresentation', {
|
||||
color: { red: 1, green: 0, blue: 0, alpha: 1 },
|
||||
textDocument,
|
||||
range: {
|
||||
start: { line: 0, character: 12 },
|
||||
end: { line: 0, character: 21 },
|
||||
},
|
||||
})
|
||||
|
||||
expect(res).toEqual([
|
||||
{ label: 'bg-[#f00]' },
|
||||
{ label: 'bg-[rgb(255,0,0)]' },
|
||||
{ label: 'bg-[hsl(0,100%,50%)]' },
|
||||
])
|
||||
})
|
||||
|
||||
test.concurrent('arbitrary hex color', async () => {
|
||||
let textDocument = await c.openDocument({ text: '<div class="bg-[#ff0000]">' })
|
||||
let res = await c.sendRequest('textDocument/colorPresentation', {
|
||||
color: { red: 1, green: 0, blue: 0, alpha: 1 },
|
||||
textDocument,
|
||||
range: {
|
||||
start: { line: 0, character: 12 },
|
||||
end: { line: 0, character: 24 },
|
||||
},
|
||||
})
|
||||
|
||||
expect(res).toEqual([
|
||||
{ label: 'bg-[#ff0000]' },
|
||||
{ label: 'bg-[rgb(255,0,0)]' },
|
||||
{ label: 'bg-[hsl(0,100%,50%)]' },
|
||||
])
|
||||
})
|
||||
|
||||
test.concurrent('arbitrary rgb color', async () => {
|
||||
let textDocument = await c.openDocument({ text: '<div class="bg-[rgb(255,0,0)]">' })
|
||||
let res = await c.sendRequest('textDocument/colorPresentation', {
|
||||
color: { red: 1, green: 0, blue: 0, alpha: 1 },
|
||||
textDocument,
|
||||
range: {
|
||||
start: { line: 0, character: 12 },
|
||||
end: { line: 0, character: 29 },
|
||||
},
|
||||
})
|
||||
|
||||
expect(res).toEqual([
|
||||
{ label: 'bg-[#ff0000]' },
|
||||
{ label: 'bg-[rgb(255,0,0)]' },
|
||||
{ label: 'bg-[hsl(0,100%,50%)]' },
|
||||
])
|
||||
})
|
||||
|
||||
test.concurrent('arbitrary hsl color', async () => {
|
||||
let textDocument = await c.openDocument({ text: '<div class="bg-[hsl(0,100%,50%)]">' })
|
||||
let res = await c.sendRequest('textDocument/colorPresentation', {
|
||||
color: { red: 1, green: 0, blue: 0, alpha: 1 },
|
||||
textDocument,
|
||||
range: {
|
||||
start: { line: 0, character: 12 },
|
||||
end: { line: 0, character: 32 },
|
||||
},
|
||||
})
|
||||
|
||||
expect(res).toEqual([
|
||||
{ label: 'bg-[#ff0000]' },
|
||||
{ label: 'bg-[rgb(255,0,0)]' },
|
||||
{ label: 'bg-[hsl(0,100%,50%)]' },
|
||||
])
|
||||
})
|
||||
})
|
|
@ -0,0 +1,231 @@
|
|||
import * as path from 'node:path'
|
||||
import * as cp from 'node:child_process'
|
||||
import * as rpc from 'vscode-jsonrpc'
|
||||
import { beforeAll } from 'vitest'
|
||||
|
||||
let settings = {}
|
||||
let initPromise
|
||||
let childProcess
|
||||
let docSettings = new Map()
|
||||
|
||||
async function init(fixture) {
|
||||
childProcess = cp.fork('./bin/tailwindcss-language-server', { silent: true })
|
||||
|
||||
const capabilities = {
|
||||
textDocument: {
|
||||
codeAction: { dynamicRegistration: true },
|
||||
codeLens: { dynamicRegistration: true },
|
||||
colorProvider: { dynamicRegistration: true },
|
||||
completion: {
|
||||
completionItem: {
|
||||
commitCharactersSupport: true,
|
||||
documentationFormat: ['markdown', 'plaintext'],
|
||||
snippetSupport: true,
|
||||
},
|
||||
completionItemKind: {
|
||||
valueSet: [
|
||||
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
|
||||
25,
|
||||
],
|
||||
},
|
||||
contextSupport: true,
|
||||
dynamicRegistration: true,
|
||||
},
|
||||
definition: { dynamicRegistration: true },
|
||||
documentHighlight: { dynamicRegistration: true },
|
||||
documentLink: { dynamicRegistration: true },
|
||||
documentSymbol: {
|
||||
dynamicRegistration: true,
|
||||
symbolKind: {
|
||||
valueSet: [
|
||||
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
|
||||
25, 26,
|
||||
],
|
||||
},
|
||||
},
|
||||
formatting: { dynamicRegistration: true },
|
||||
hover: {
|
||||
contentFormat: ['markdown', 'plaintext'],
|
||||
dynamicRegistration: true,
|
||||
},
|
||||
implementation: { dynamicRegistration: true },
|
||||
onTypeFormatting: { dynamicRegistration: true },
|
||||
publishDiagnostics: { relatedInformation: true },
|
||||
rangeFormatting: { dynamicRegistration: true },
|
||||
references: { dynamicRegistration: true },
|
||||
rename: { dynamicRegistration: true },
|
||||
signatureHelp: {
|
||||
dynamicRegistration: true,
|
||||
signatureInformation: { documentationFormat: ['markdown', 'plaintext'] },
|
||||
},
|
||||
synchronization: {
|
||||
didSave: true,
|
||||
dynamicRegistration: true,
|
||||
willSave: true,
|
||||
willSaveWaitUntil: true,
|
||||
},
|
||||
typeDefinition: { dynamicRegistration: true },
|
||||
},
|
||||
workspace: {
|
||||
applyEdit: true,
|
||||
configuration: true,
|
||||
didChangeConfiguration: { dynamicRegistration: true },
|
||||
didChangeWatchedFiles: { dynamicRegistration: true },
|
||||
executeCommand: { dynamicRegistration: true },
|
||||
symbol: {
|
||||
dynamicRegistration: true,
|
||||
symbolKind: {
|
||||
valueSet: [
|
||||
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
|
||||
25, 26,
|
||||
],
|
||||
},
|
||||
},
|
||||
workspaceEdit: { documentChanges: true },
|
||||
workspaceFolders: true,
|
||||
},
|
||||
}
|
||||
|
||||
let connection = rpc.createMessageConnection(
|
||||
new rpc.StreamMessageReader(childProcess.stdout),
|
||||
new rpc.StreamMessageWriter(childProcess.stdin)
|
||||
)
|
||||
|
||||
connection.listen()
|
||||
|
||||
await connection.sendRequest(new rpc.RequestType('initialize'), {
|
||||
processId: -1,
|
||||
// rootPath: '.',
|
||||
rootUri: `file://${path.resolve('./tests/fixtures/', fixture)}`,
|
||||
capabilities,
|
||||
trace: 'off',
|
||||
workspaceFolders: [],
|
||||
initializationOptions: {
|
||||
testMode: true,
|
||||
},
|
||||
})
|
||||
|
||||
await connection.sendNotification(new rpc.NotificationType('initialized'))
|
||||
|
||||
connection.onRequest(new rpc.RequestType('workspace/configuration'), (params) => {
|
||||
return params.items.map((item) => {
|
||||
if (docSettings.has(item.scopeUri)) {
|
||||
return docSettings.get(item.scopeUri)[item.section] ?? {}
|
||||
}
|
||||
return settings[item.section] ?? {}
|
||||
})
|
||||
})
|
||||
|
||||
initPromise = new Promise((resolve) => {
|
||||
connection.onRequest(new rpc.RequestType('client/registerCapability'), ({ registrations }) => {
|
||||
if (registrations.findIndex((r) => r.method === 'textDocument/completion') > -1) {
|
||||
resolve()
|
||||
}
|
||||
return null
|
||||
})
|
||||
})
|
||||
|
||||
let counter = 0
|
||||
|
||||
return {
|
||||
connection,
|
||||
sendRequest(type, params) {
|
||||
return connection.sendRequest(new rpc.RequestType(type), params)
|
||||
},
|
||||
onNotification(type, callback) {
|
||||
return connection.onNotification(new rpc.RequestType(type), callback)
|
||||
},
|
||||
async openDocument({ text, lang = 'html', dir = '', settings = {} }) {
|
||||
let uri = `file://${path.resolve('./tests/fixtures', fixture, dir, `file-${counter++}`)}`
|
||||
docSettings.set(uri, settings)
|
||||
await connection.sendNotification(new rpc.NotificationType('textDocument/didOpen'), {
|
||||
textDocument: {
|
||||
uri,
|
||||
languageId: lang,
|
||||
version: 1,
|
||||
text,
|
||||
},
|
||||
})
|
||||
|
||||
await initPromise
|
||||
|
||||
return {
|
||||
uri,
|
||||
async updateSettings(settings) {
|
||||
docSettings.set(uri, settings)
|
||||
await connection.sendNotification(
|
||||
new rpc.NotificationType('workspace/didChangeConfiguration')
|
||||
)
|
||||
},
|
||||
}
|
||||
},
|
||||
async updateSettings(newSettings) {
|
||||
settings = newSettings
|
||||
await connection.sendNotification(
|
||||
new rpc.NotificationType('workspace/didChangeConfiguration')
|
||||
)
|
||||
},
|
||||
async updateFile(file, text) {
|
||||
let uri = `file://${path.resolve('./tests/fixtures', fixture, file)}`
|
||||
|
||||
await connection.sendNotification(new rpc.NotificationType('textDocument/didChange'), {
|
||||
textDocument: { uri, version: counter++ },
|
||||
contentChanges: [{ text }],
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function withFixture(fixture, callback) {
|
||||
let c
|
||||
|
||||
beforeAll(async () => {
|
||||
c = await init(fixture)
|
||||
return () => c.connection.end()
|
||||
})
|
||||
|
||||
callback({
|
||||
get connection() {
|
||||
return c.connection
|
||||
},
|
||||
get sendRequest() {
|
||||
return c.sendRequest
|
||||
},
|
||||
get onNotification() {
|
||||
return c.onNotification
|
||||
},
|
||||
get openDocument() {
|
||||
return c.openDocument
|
||||
},
|
||||
get updateSettings() {
|
||||
return c.updateSettings
|
||||
},
|
||||
get updateFile() {
|
||||
return c.updateFile
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// let counter = 0
|
||||
|
||||
// export async function openDocument(connection, fixture, languageId, text) {
|
||||
// let uri = `file://${path.resolve('./tests/fixtures', fixture, `file-${counter++}`)}`
|
||||
|
||||
// await connection.sendNotification(new rpc.NotificationType('textDocument/didOpen'), {
|
||||
// textDocument: {
|
||||
// uri,
|
||||
// languageId,
|
||||
// version: 1,
|
||||
// text,
|
||||
// },
|
||||
// })
|
||||
|
||||
// await initPromise
|
||||
|
||||
// return uri
|
||||
// }
|
||||
|
||||
// export async function updateSettings(connection, newSettings) {
|
||||
// settings = newSettings
|
||||
// await connection.sendNotification(new rpc.NotificationType('workspace/didChangeConfiguration'))
|
||||
// }
|
|
@ -0,0 +1,84 @@
|
|||
import { expect, test } from 'vitest'
|
||||
import { withFixture } from '../common'
|
||||
|
||||
withFixture('dependencies', (c) => {
|
||||
async function completion({
|
||||
lang,
|
||||
text,
|
||||
position,
|
||||
context = {
|
||||
triggerKind: 1,
|
||||
},
|
||||
settings,
|
||||
}) {
|
||||
let textDocument = await c.openDocument({ text, lang, settings })
|
||||
|
||||
return c.sendRequest('textDocument/completion', {
|
||||
textDocument,
|
||||
position,
|
||||
context,
|
||||
})
|
||||
}
|
||||
|
||||
test.concurrent('@config', async () => {
|
||||
let result = await completion({
|
||||
text: '@config "',
|
||||
lang: 'css',
|
||||
position: {
|
||||
line: 0,
|
||||
character: 9,
|
||||
},
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
isIncomplete: false,
|
||||
items: [
|
||||
{
|
||||
label: 'sub-dir/',
|
||||
kind: 19,
|
||||
command: { command: 'editor.action.triggerSuggest', title: '' },
|
||||
data: expect.anything(),
|
||||
textEdit: {
|
||||
newText: 'sub-dir/',
|
||||
range: { start: { line: 0, character: 9 }, end: { line: 0, character: 9 } },
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'tailwind.config.js',
|
||||
kind: 17,
|
||||
data: expect.anything(),
|
||||
textEdit: {
|
||||
newText: 'tailwind.config.js',
|
||||
range: { start: { line: 0, character: 9 }, end: { line: 0, character: 9 } },
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
test.concurrent('@config directory', async () => {
|
||||
let result = await completion({
|
||||
text: '@config "./sub-dir/',
|
||||
lang: 'css',
|
||||
position: {
|
||||
line: 0,
|
||||
character: 19,
|
||||
},
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
isIncomplete: false,
|
||||
items: [
|
||||
{
|
||||
label: 'colors.js',
|
||||
kind: 17,
|
||||
data: expect.anything(),
|
||||
textEdit: {
|
||||
newText: 'colors.js',
|
||||
range: { start: { line: 0, character: 19 }, end: { line: 0, character: 19 } },
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,121 @@
|
|||
import { expect, test } from 'vitest'
|
||||
import { withFixture } from '../common'
|
||||
|
||||
withFixture('basic', (c) => {
|
||||
async function completion({
|
||||
lang,
|
||||
text,
|
||||
position,
|
||||
context = {
|
||||
triggerKind: 1,
|
||||
},
|
||||
settings,
|
||||
}) {
|
||||
let textDocument = await c.openDocument({ text, lang, settings })
|
||||
|
||||
return c.sendRequest('textDocument/completion', {
|
||||
textDocument,
|
||||
position,
|
||||
context,
|
||||
})
|
||||
}
|
||||
|
||||
async function expectCompletions({ lang, text, position, settings }) {
|
||||
let result = await completion({ lang, text, position, settings })
|
||||
let textEdit = expect.objectContaining({ range: { start: position, end: position } })
|
||||
|
||||
expect(result.items.length).toBe(11175)
|
||||
expect(result.items.filter((item) => item.label.endsWith(':')).length).toBe(157)
|
||||
expect(result).toEqual({
|
||||
isIncomplete: false,
|
||||
items: expect.arrayContaining([
|
||||
expect.objectContaining({ label: 'hover:', textEdit }),
|
||||
expect.objectContaining({ label: 'uppercase', textEdit }),
|
||||
]),
|
||||
})
|
||||
}
|
||||
|
||||
test.concurrent('HTML', async () => {
|
||||
await expectCompletions({ text: '<div class=""></div>', position: { line: 0, character: 12 } })
|
||||
})
|
||||
|
||||
test.concurrent('JSX', async () => {
|
||||
await expectCompletions({
|
||||
lang: 'javascriptreact',
|
||||
text: "<div className={''}></div>",
|
||||
position: {
|
||||
line: 0,
|
||||
character: 17,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test.concurrent('JSX concatination', async () => {
|
||||
await expectCompletions({
|
||||
lang: 'javascriptreact',
|
||||
text: "<div className={'' + ''}></div>",
|
||||
position: {
|
||||
line: 0,
|
||||
character: 22,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test.concurrent('JSX outside strings', async () => {
|
||||
let result = await completion({
|
||||
lang: 'javascriptreact',
|
||||
text: "<div className={'' + ''}></div>",
|
||||
position: {
|
||||
line: 0,
|
||||
character: 18,
|
||||
},
|
||||
})
|
||||
expect(result).toBe(null)
|
||||
})
|
||||
|
||||
test.concurrent('classRegex simple', async () => {
|
||||
await expectCompletions({
|
||||
text: 'test ',
|
||||
position: {
|
||||
line: 0,
|
||||
character: 5,
|
||||
},
|
||||
settings: { tailwindCSS: { experimental: { classRegex: ['test (\\S*)'] } } },
|
||||
})
|
||||
})
|
||||
|
||||
test.concurrent('classRegex nested', async () => {
|
||||
await expectCompletions({
|
||||
text: 'test ""',
|
||||
position: {
|
||||
line: 0,
|
||||
character: 6,
|
||||
},
|
||||
settings: {
|
||||
tailwindCSS: { experimental: { classRegex: [['test (\\S*)', '"([^"]*)"']] } },
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test.concurrent('resolve', async () => {
|
||||
let result = await completion({
|
||||
text: '<div class="">',
|
||||
position: {
|
||||
line: 0,
|
||||
character: 12,
|
||||
},
|
||||
})
|
||||
|
||||
let item = result.items.find((item) => item.label === 'uppercase')
|
||||
let resolved = await c.sendRequest('completionItem/resolve', item)
|
||||
|
||||
expect(resolved).toEqual({
|
||||
...item,
|
||||
detail: 'text-transform: uppercase;',
|
||||
documentation: {
|
||||
kind: 'markdown',
|
||||
value: '```css\n.uppercase {\n text-transform: uppercase;\n}\n```',
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"code": ".test { @apply uppercase; color: red; @apply lowercase }",
|
||||
"language": "css",
|
||||
"expected": []
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"code": ".test { @apply uppercase }\n.test { @apply lowercase }",
|
||||
"language": "css",
|
||||
"expected": []
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
{
|
||||
"code": ".test { @apply uppercase lowercase }",
|
||||
"language": "css",
|
||||
"expected": [
|
||||
{
|
||||
"code": "cssConflict",
|
||||
"className": {
|
||||
"className": "uppercase",
|
||||
"classList": {
|
||||
"classList": "uppercase lowercase",
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 15 },
|
||||
"end": { "line": 0, "character": 34 }
|
||||
},
|
||||
"important": false
|
||||
},
|
||||
"relativeRange": {
|
||||
"start": { "line": 0, "character": 0 },
|
||||
"end": { "line": 0, "character": 9 }
|
||||
},
|
||||
"range": { "start": { "line": 0, "character": 15 }, "end": { "line": 0, "character": 24 } }
|
||||
},
|
||||
"otherClassNames": [
|
||||
{
|
||||
"className": "lowercase",
|
||||
"classList": {
|
||||
"classList": "uppercase lowercase",
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 15 },
|
||||
"end": { "line": 0, "character": 34 }
|
||||
},
|
||||
"important": false
|
||||
},
|
||||
"relativeRange": {
|
||||
"start": { "line": 0, "character": 10 },
|
||||
"end": { "line": 0, "character": 19 }
|
||||
},
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 25 },
|
||||
"end": { "line": 0, "character": 34 }
|
||||
}
|
||||
}
|
||||
],
|
||||
"range": { "start": { "line": 0, "character": 15 }, "end": { "line": 0, "character": 24 } },
|
||||
"severity": 2,
|
||||
"message": "'uppercase' applies the same CSS properties as 'lowercase'.",
|
||||
"relatedInformation": [
|
||||
{
|
||||
"message": "lowercase",
|
||||
"location": {
|
||||
"uri": "{{URI}}",
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 25 },
|
||||
"end": { "line": 0, "character": 34 }
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "cssConflict",
|
||||
"className": {
|
||||
"className": "lowercase",
|
||||
"classList": {
|
||||
"classList": "uppercase lowercase",
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 15 },
|
||||
"end": { "line": 0, "character": 34 }
|
||||
},
|
||||
"important": false
|
||||
},
|
||||
"relativeRange": {
|
||||
"start": { "line": 0, "character": 10 },
|
||||
"end": { "line": 0, "character": 19 }
|
||||
},
|
||||
"range": { "start": { "line": 0, "character": 25 }, "end": { "line": 0, "character": 34 } }
|
||||
},
|
||||
"otherClassNames": [
|
||||
{
|
||||
"className": "uppercase",
|
||||
"classList": {
|
||||
"classList": "uppercase lowercase",
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 15 },
|
||||
"end": { "line": 0, "character": 34 }
|
||||
},
|
||||
"important": false
|
||||
},
|
||||
"relativeRange": {
|
||||
"start": { "line": 0, "character": 0 },
|
||||
"end": { "line": 0, "character": 9 }
|
||||
},
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 15 },
|
||||
"end": { "line": 0, "character": 24 }
|
||||
}
|
||||
}
|
||||
],
|
||||
"range": { "start": { "line": 0, "character": 25 }, "end": { "line": 0, "character": 34 } },
|
||||
"severity": 2,
|
||||
"message": "'lowercase' applies the same CSS properties as 'uppercase'.",
|
||||
"relatedInformation": [
|
||||
{
|
||||
"message": "uppercase",
|
||||
"location": {
|
||||
"uri": "{{URI}}",
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 15 },
|
||||
"end": { "line": 0, "character": 24 }
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"code": "<div className={'lowercase' + 'uppercase'}>",
|
||||
"language": "javascriptreact",
|
||||
"expected": []
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
{
|
||||
"code": "<div className={'lowercase uppercase' + 'uppercase'}>",
|
||||
"language": "javascriptreact",
|
||||
"expected": [
|
||||
{
|
||||
"code": "cssConflict",
|
||||
"className": {
|
||||
"className": "lowercase",
|
||||
"classList": {
|
||||
"classList": "lowercase uppercase",
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 17 },
|
||||
"end": { "line": 0, "character": 36 }
|
||||
}
|
||||
},
|
||||
"relativeRange": {
|
||||
"start": { "line": 0, "character": 0 },
|
||||
"end": { "line": 0, "character": 9 }
|
||||
},
|
||||
"range": { "start": { "line": 0, "character": 17 }, "end": { "line": 0, "character": 26 } }
|
||||
},
|
||||
"otherClassNames": [
|
||||
{
|
||||
"className": "uppercase",
|
||||
"classList": {
|
||||
"classList": "lowercase uppercase",
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 17 },
|
||||
"end": { "line": 0, "character": 36 }
|
||||
}
|
||||
},
|
||||
"relativeRange": {
|
||||
"start": { "line": 0, "character": 10 },
|
||||
"end": { "line": 0, "character": 19 }
|
||||
},
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 27 },
|
||||
"end": { "line": 0, "character": 36 }
|
||||
}
|
||||
}
|
||||
],
|
||||
"range": { "start": { "line": 0, "character": 17 }, "end": { "line": 0, "character": 26 } },
|
||||
"severity": 2,
|
||||
"message": "'lowercase' applies the same CSS properties as 'uppercase'.",
|
||||
"relatedInformation": [
|
||||
{
|
||||
"message": "uppercase",
|
||||
"location": {
|
||||
"uri": "{{URI}}",
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 27 },
|
||||
"end": { "line": 0, "character": 36 }
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "cssConflict",
|
||||
"className": {
|
||||
"className": "uppercase",
|
||||
"classList": {
|
||||
"classList": "lowercase uppercase",
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 17 },
|
||||
"end": { "line": 0, "character": 36 }
|
||||
}
|
||||
},
|
||||
"relativeRange": {
|
||||
"start": { "line": 0, "character": 10 },
|
||||
"end": { "line": 0, "character": 19 }
|
||||
},
|
||||
"range": { "start": { "line": 0, "character": 27 }, "end": { "line": 0, "character": 36 } }
|
||||
},
|
||||
"otherClassNames": [
|
||||
{
|
||||
"className": "lowercase",
|
||||
"classList": {
|
||||
"classList": "lowercase uppercase",
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 17 },
|
||||
"end": { "line": 0, "character": 36 }
|
||||
}
|
||||
},
|
||||
"relativeRange": {
|
||||
"start": { "line": 0, "character": 0 },
|
||||
"end": { "line": 0, "character": 9 }
|
||||
},
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 17 },
|
||||
"end": { "line": 0, "character": 26 }
|
||||
}
|
||||
}
|
||||
],
|
||||
"range": { "start": { "line": 0, "character": 27 }, "end": { "line": 0, "character": 36 } },
|
||||
"severity": 2,
|
||||
"message": "'uppercase' applies the same CSS properties as 'lowercase'.",
|
||||
"relatedInformation": [
|
||||
{
|
||||
"message": "lowercase",
|
||||
"location": {
|
||||
"uri": "{{URI}}",
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 17 },
|
||||
"end": { "line": 0, "character": 26 }
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
{
|
||||
"code": "<div class=\"uppercase lowercase\"></div>",
|
||||
"expected": [
|
||||
{
|
||||
"code": "cssConflict",
|
||||
"className": {
|
||||
"className": "uppercase",
|
||||
"classList": {
|
||||
"classList": "uppercase lowercase",
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 12 },
|
||||
"end": { "line": 0, "character": 31 }
|
||||
}
|
||||
},
|
||||
"relativeRange": {
|
||||
"start": { "line": 0, "character": 0 },
|
||||
"end": { "line": 0, "character": 9 }
|
||||
},
|
||||
"range": { "start": { "line": 0, "character": 12 }, "end": { "line": 0, "character": 21 } }
|
||||
},
|
||||
"otherClassNames": [
|
||||
{
|
||||
"className": "lowercase",
|
||||
"classList": {
|
||||
"classList": "uppercase lowercase",
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 12 },
|
||||
"end": { "line": 0, "character": 31 }
|
||||
}
|
||||
},
|
||||
"relativeRange": {
|
||||
"start": { "line": 0, "character": 10 },
|
||||
"end": { "line": 0, "character": 19 }
|
||||
},
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 22 },
|
||||
"end": { "line": 0, "character": 31 }
|
||||
}
|
||||
}
|
||||
],
|
||||
"range": { "start": { "line": 0, "character": 12 }, "end": { "line": 0, "character": 21 } },
|
||||
"severity": 2,
|
||||
"message": "'uppercase' applies the same CSS properties as 'lowercase'.",
|
||||
"relatedInformation": [
|
||||
{
|
||||
"message": "lowercase",
|
||||
"location": {
|
||||
"uri": "{{URI}}",
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 22 },
|
||||
"end": { "line": 0, "character": 31 }
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "cssConflict",
|
||||
"className": {
|
||||
"className": "lowercase",
|
||||
"classList": {
|
||||
"classList": "uppercase lowercase",
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 12 },
|
||||
"end": { "line": 0, "character": 31 }
|
||||
}
|
||||
},
|
||||
"relativeRange": {
|
||||
"start": { "line": 0, "character": 10 },
|
||||
"end": { "line": 0, "character": 19 }
|
||||
},
|
||||
"range": { "start": { "line": 0, "character": 22 }, "end": { "line": 0, "character": 31 } }
|
||||
},
|
||||
"otherClassNames": [
|
||||
{
|
||||
"className": "uppercase",
|
||||
"classList": {
|
||||
"classList": "uppercase lowercase",
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 12 },
|
||||
"end": { "line": 0, "character": 31 }
|
||||
}
|
||||
},
|
||||
"relativeRange": {
|
||||
"start": { "line": 0, "character": 0 },
|
||||
"end": { "line": 0, "character": 9 }
|
||||
},
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 12 },
|
||||
"end": { "line": 0, "character": 21 }
|
||||
}
|
||||
}
|
||||
],
|
||||
"range": { "start": { "line": 0, "character": 22 }, "end": { "line": 0, "character": 31 } },
|
||||
"severity": 2,
|
||||
"message": "'lowercase' applies the same CSS properties as 'uppercase'.",
|
||||
"relatedInformation": [
|
||||
{
|
||||
"message": "uppercase",
|
||||
"location": {
|
||||
"uri": "{{URI}}",
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 12 },
|
||||
"end": { "line": 0, "character": 21 }
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"code": "<div class=\"uppercase sm:lowercase\"></div>",
|
||||
"expected": []
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
{
|
||||
"code": "<div class=\"sm:uppercase sm:lowercase\"></div>",
|
||||
"expected": [
|
||||
{
|
||||
"code": "cssConflict",
|
||||
"className": {
|
||||
"className": "sm:uppercase",
|
||||
"classList": {
|
||||
"classList": "sm:uppercase sm:lowercase",
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 12 },
|
||||
"end": { "line": 0, "character": 37 }
|
||||
}
|
||||
},
|
||||
"relativeRange": {
|
||||
"start": { "line": 0, "character": 0 },
|
||||
"end": { "line": 0, "character": 12 }
|
||||
},
|
||||
"range": { "start": { "line": 0, "character": 12 }, "end": { "line": 0, "character": 24 } }
|
||||
},
|
||||
"otherClassNames": [
|
||||
{
|
||||
"className": "sm:lowercase",
|
||||
"classList": {
|
||||
"classList": "sm:uppercase sm:lowercase",
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 12 },
|
||||
"end": { "line": 0, "character": 37 }
|
||||
}
|
||||
},
|
||||
"relativeRange": {
|
||||
"start": { "line": 0, "character": 13 },
|
||||
"end": { "line": 0, "character": 25 }
|
||||
},
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 25 },
|
||||
"end": { "line": 0, "character": 37 }
|
||||
}
|
||||
}
|
||||
],
|
||||
"range": { "start": { "line": 0, "character": 12 }, "end": { "line": 0, "character": 24 } },
|
||||
"severity": 2,
|
||||
"message": "'sm:uppercase' applies the same CSS properties as 'sm:lowercase'.",
|
||||
"relatedInformation": [
|
||||
{
|
||||
"message": "sm:lowercase",
|
||||
"location": {
|
||||
"uri": "{{URI}}",
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 25 },
|
||||
"end": { "line": 0, "character": 37 }
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "cssConflict",
|
||||
"className": {
|
||||
"className": "sm:lowercase",
|
||||
"classList": {
|
||||
"classList": "sm:uppercase sm:lowercase",
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 12 },
|
||||
"end": { "line": 0, "character": 37 }
|
||||
}
|
||||
},
|
||||
"relativeRange": {
|
||||
"start": { "line": 0, "character": 13 },
|
||||
"end": { "line": 0, "character": 25 }
|
||||
},
|
||||
"range": { "start": { "line": 0, "character": 25 }, "end": { "line": 0, "character": 37 } }
|
||||
},
|
||||
"otherClassNames": [
|
||||
{
|
||||
"className": "sm:uppercase",
|
||||
"classList": {
|
||||
"classList": "sm:uppercase sm:lowercase",
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 12 },
|
||||
"end": { "line": 0, "character": 37 }
|
||||
}
|
||||
},
|
||||
"relativeRange": {
|
||||
"start": { "line": 0, "character": 0 },
|
||||
"end": { "line": 0, "character": 12 }
|
||||
},
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 12 },
|
||||
"end": { "line": 0, "character": 24 }
|
||||
}
|
||||
}
|
||||
],
|
||||
"range": { "start": { "line": 0, "character": 25 }, "end": { "line": 0, "character": 37 } },
|
||||
"severity": 2,
|
||||
"message": "'sm:lowercase' applies the same CSS properties as 'sm:uppercase'.",
|
||||
"relatedInformation": [
|
||||
{
|
||||
"message": "sm:uppercase",
|
||||
"location": {
|
||||
"uri": "{{URI}}",
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 12 },
|
||||
"end": { "line": 0, "character": 24 }
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
import { expect, test } from 'vitest'
|
||||
import { withFixture } from '../common'
|
||||
import * as fs from 'node:fs/promises'
|
||||
|
||||
withFixture('basic', (c) => {
|
||||
function testFixture(fixture) {
|
||||
test(fixture, async () => {
|
||||
fixture = await fs.readFile(`tests/diagnostics/${fixture}.json`, 'utf8')
|
||||
|
||||
let { code, expected, language = 'html' } = JSON.parse(fixture)
|
||||
|
||||
let promise = new Promise((resolve) => {
|
||||
c.onNotification('textDocument/publishDiagnostics', ({ diagnostics }) => {
|
||||
resolve(diagnostics)
|
||||
})
|
||||
})
|
||||
|
||||
let doc = await c.openDocument({ text: code, lang: language })
|
||||
let diagnostics = await promise
|
||||
|
||||
expected = JSON.parse(JSON.stringify(expected).replaceAll('{{URI}}', doc.uri))
|
||||
|
||||
expect(diagnostics).toEqual(expected)
|
||||
})
|
||||
}
|
||||
|
||||
testFixture('css-conflict/simple')
|
||||
testFixture('css-conflict/variants-negative')
|
||||
testFixture('css-conflict/variants-positive')
|
||||
testFixture('css-conflict/jsx-concat-negative')
|
||||
testFixture('css-conflict/jsx-concat-positive')
|
||||
testFixture('css-conflict/css')
|
||||
testFixture('css-conflict/css-multi-rule')
|
||||
testFixture('css-conflict/css-multi-prop')
|
||||
testFixture('invalid-screen/simple')
|
||||
testFixture('invalid-theme/simple')
|
||||
})
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"code": "@screen small",
|
||||
"language": "css",
|
||||
"expected": [
|
||||
{
|
||||
"code": "invalidScreen",
|
||||
"range": { "start": { "line": 0, "character": 8 }, "end": { "line": 0, "character": 13 } },
|
||||
"severity": 1,
|
||||
"message": "The screen 'small' does not exist in your theme config. Did you mean 'sm'?",
|
||||
"suggestions": ["sm"]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"code": ".test { color: theme(colors.red.901) }",
|
||||
"language": "css",
|
||||
"expected": [
|
||||
{
|
||||
"code": "invalidConfigPath",
|
||||
"range": { "start": { "line": 0, "character": 21 }, "end": { "line": 0, "character": 35 } },
|
||||
"severity": 1,
|
||||
"message": "'colors.red.901' does not exist in your theme config. Did you mean 'colors.red.900'?",
|
||||
"suggestions": ["colors.red.900"]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
import { test, expect } from 'vitest'
|
||||
import { withFixture } from '../common'
|
||||
import * as path from 'path'
|
||||
|
||||
withFixture('basic', (c) => {
|
||||
async function testDocumentLinks(name, { text, lang, expected }) {
|
||||
test.concurrent(name, async () => {
|
||||
let textDocument = await c.openDocument({ text, lang })
|
||||
let res = await c.sendRequest('textDocument/documentLink', {
|
||||
textDocument,
|
||||
})
|
||||
|
||||
expect(res).toEqual(expected)
|
||||
})
|
||||
}
|
||||
|
||||
testDocumentLinks('file exists', {
|
||||
text: '@config "tailwind.config.js";',
|
||||
lang: 'css',
|
||||
expected: [
|
||||
{
|
||||
target: `file://${path
|
||||
.resolve('./tests/fixtures/basic/tailwind.config.js')
|
||||
.replace(/@/g, '%40')}`,
|
||||
range: { start: { line: 0, character: 8 }, end: { line: 0, character: 28 } },
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
testDocumentLinks('file does not exist', {
|
||||
text: '@config "does-not-exist.js";',
|
||||
lang: 'css',
|
||||
expected: [
|
||||
{
|
||||
target: `file://${path
|
||||
.resolve('./tests/fixtures/basic/does-not-exist.js')
|
||||
.replace(/@/g, '%40')}`,
|
||||
range: { start: { line: 0, character: 8 }, end: { line: 0, character: 27 } },
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
|
@ -0,0 +1,38 @@
|
|||
import { test, expect } from 'vitest'
|
||||
import { withFixture } from '../common'
|
||||
|
||||
withFixture('multi-config-content', (c) => {
|
||||
test.concurrent('multi-config with content config - 1', async () => {
|
||||
let textDocument = await c.openDocument({ text: '<div class="bg-foo">', dir: 'one' })
|
||||
let res = await c.sendRequest('textDocument/hover', {
|
||||
textDocument,
|
||||
position: { line: 0, character: 13 },
|
||||
})
|
||||
|
||||
expect(res).toEqual({
|
||||
contents: {
|
||||
language: 'css',
|
||||
value:
|
||||
'.bg-foo {\n --tw-bg-opacity: 1;\n background-color: rgb(255 0 0 / var(--tw-bg-opacity));\n}',
|
||||
},
|
||||
range: { start: { line: 0, character: 12 }, end: { line: 0, character: 18 } },
|
||||
})
|
||||
})
|
||||
|
||||
test.concurrent('multi-config with content config - 2', async () => {
|
||||
let textDocument = await c.openDocument({ text: '<div class="bg-foo">', dir: 'two' })
|
||||
let res = await c.sendRequest('textDocument/hover', {
|
||||
textDocument,
|
||||
position: { line: 0, character: 13 },
|
||||
})
|
||||
|
||||
expect(res).toEqual({
|
||||
contents: {
|
||||
language: 'css',
|
||||
value:
|
||||
'.bg-foo {\n --tw-bg-opacity: 1;\n background-color: rgb(0 0 255 / var(--tw-bg-opacity));\n}',
|
||||
},
|
||||
range: { start: { line: 0, character: 12 }, end: { line: 0, character: 18 } },
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,38 @@
|
|||
import { test, expect } from 'vitest'
|
||||
import { withFixture } from '../common'
|
||||
|
||||
withFixture('multi-config', (c) => {
|
||||
test.concurrent('multi-config 1', async () => {
|
||||
let textDocument = await c.openDocument({ text: '<div class="bg-foo">', dir: 'one' })
|
||||
let res = await c.sendRequest('textDocument/hover', {
|
||||
textDocument,
|
||||
position: { line: 0, character: 13 },
|
||||
})
|
||||
|
||||
expect(res).toEqual({
|
||||
contents: {
|
||||
language: 'css',
|
||||
value:
|
||||
'.bg-foo {\n --tw-bg-opacity: 1;\n background-color: rgb(255 0 0 / var(--tw-bg-opacity));\n}',
|
||||
},
|
||||
range: { start: { line: 0, character: 12 }, end: { line: 0, character: 18 } },
|
||||
})
|
||||
})
|
||||
|
||||
test.concurrent('multi-config 2', async () => {
|
||||
let textDocument = await c.openDocument({ text: '<div class="bg-foo">', dir: 'two' })
|
||||
let res = await c.sendRequest('textDocument/hover', {
|
||||
textDocument,
|
||||
position: { line: 0, character: 13 },
|
||||
})
|
||||
|
||||
expect(res).toEqual({
|
||||
contents: {
|
||||
language: 'css',
|
||||
value:
|
||||
'.bg-foo {\n --tw-bg-opacity: 1;\n background-color: rgb(0 0 255 / var(--tw-bg-opacity));\n}',
|
||||
},
|
||||
range: { start: { line: 0, character: 12 }, end: { line: 0, character: 18 } },
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1 @@
|
|||
module.exports = {}
|
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
foo: 'red',
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
const colors = require('./sub-dir/colors')
|
||||
|
||||
module.exports = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors,
|
||||
},
|
||||
},
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
module.exports = {
|
||||
content: ['./one/**/*'],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
foo: 'red',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
module.exports = {
|
||||
content: ['./two/**/*'],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
foo: 'blue',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
module.exports = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
foo: 'red',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
module.exports = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
foo: 'blue',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
1003
packages/tailwindcss-language-server/tests/fixtures/v1/package-lock.json
generated
vendored
100644
1003
packages/tailwindcss-language-server/tests/fixtures/v1/package-lock.json
generated
vendored
100644
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"tailwindcss": "1.9.6"
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
module.exports = {}
|
1540
packages/tailwindcss-language-server/tests/fixtures/v2-jit/package-lock.json
generated
vendored
100644
1540
packages/tailwindcss-language-server/tests/fixtures/v2-jit/package-lock.json
generated
vendored
100644
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"tailwindcss": "2.2.19"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
mode: 'jit',
|
||||
}
|
1540
packages/tailwindcss-language-server/tests/fixtures/v2/package-lock.json
generated
vendored
100644
1540
packages/tailwindcss-language-server/tests/fixtures/v2/package-lock.json
generated
vendored
100644
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"tailwindcss": "2.2.19"
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
module.exports = {}
|
|
@ -0,0 +1,78 @@
|
|||
import { test, expect } from 'vitest'
|
||||
import { withFixture } from '../common'
|
||||
|
||||
withFixture('basic', (c) => {
|
||||
async function testHover(name, { text, lang, position, expected, expectedRange, settings }) {
|
||||
test.concurrent(name, async () => {
|
||||
let textDocument = await c.openDocument({ text, lang, settings })
|
||||
let res = await c.sendRequest('textDocument/hover', {
|
||||
textDocument,
|
||||
position,
|
||||
})
|
||||
|
||||
expect(res).toEqual(
|
||||
expected
|
||||
? {
|
||||
contents: {
|
||||
language: 'css',
|
||||
value: expected,
|
||||
},
|
||||
range: expectedRange,
|
||||
}
|
||||
: expected
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
testHover('disabled', {
|
||||
text: '<div class="bg-red-500">',
|
||||
settings: {
|
||||
tailwindCSS: { hovers: false },
|
||||
},
|
||||
expected: null,
|
||||
})
|
||||
|
||||
testHover('hover', {
|
||||
text: '<div class="bg-red-500">',
|
||||
position: { line: 0, character: 13 },
|
||||
expected:
|
||||
'.bg-red-500 {\n' +
|
||||
' --tw-bg-opacity: 1;\n' +
|
||||
' background-color: rgb(239 68 68 / var(--tw-bg-opacity));\n' +
|
||||
'}',
|
||||
expectedRange: {
|
||||
start: { line: 0, character: 12 },
|
||||
end: { line: 0, character: 22 },
|
||||
},
|
||||
})
|
||||
|
||||
testHover('arbitrary value', {
|
||||
text: '<div class="p-[3px]">',
|
||||
position: { line: 0, character: 13 },
|
||||
expected: '.p-\\[3px\\] {\n' + ' padding: 3px;\n' + '}',
|
||||
expectedRange: {
|
||||
start: { line: 0, character: 12 },
|
||||
end: { line: 0, character: 19 },
|
||||
},
|
||||
})
|
||||
|
||||
testHover('arbitrary value with theme function', {
|
||||
text: '<div class="p-[theme(spacing.4)]">',
|
||||
position: { line: 0, character: 13 },
|
||||
expected: '.p-\\[theme\\(spacing\\.4\\)\\] {\n' + ' padding: 1rem/* 16px */;\n' + '}',
|
||||
expectedRange: {
|
||||
start: { line: 0, character: 12 },
|
||||
end: { line: 0, character: 32 },
|
||||
},
|
||||
})
|
||||
|
||||
testHover('arbitrary property', {
|
||||
text: '<div class="[text-wrap:balance]">',
|
||||
position: { line: 0, character: 13 },
|
||||
expected: '.\\[text-wrap\\:balance\\] {\n' + ' text-wrap: balance;\n' + '}',
|
||||
expectedRange: {
|
||||
start: { line: 0, character: 12 },
|
||||
end: { line: 0, character: 31 },
|
||||
},
|
||||
})
|
||||
})
|
|
@ -0,0 +1,9 @@
|
|||
const glob = require('fast-glob')
|
||||
const path = require('path')
|
||||
const childProcess = require('child_process')
|
||||
|
||||
const fixtures = glob.sync('tests/fixtures/*/package.json')
|
||||
|
||||
for (let fixture of fixtures) {
|
||||
childProcess.execSync('npm install', { cwd: path.dirname(fixture) })
|
||||
}
|
Loading…
Reference in New Issue