Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 75 additions & 23 deletions .github/workflows/principal.yml
Original file line number Diff line number Diff line change
@@ -1,36 +1,88 @@
name: Testes Unitários
name: Integração Contínua

on:
pull_request:
branches: [ principal ]
branches: [principal]
push:
branches: [ principal ]
branches: [principal]

permissions:
contents: read

jobs:
checks:
name: Verificações
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: '22'
cache: yarn

- name: Instalar dependências
run: yarn install --frozen-lockfile

- name: Typecheck
run: yarn typecheck

- name: Lint
run: yarn lint

- name: Build
run: yarn build

testes-unitarios:
name: Testes Unitários
needs: checks
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'yarn'

- name: Yarn - Dependências
run: yarn install --frozen-lockfile

- name: Testes unitários com cobertura
run: yarn testes-unitarios --coverage --coverageReporters=json-summary --coverageReporters=text

- name: Salvar resultados de cobertura
if: github.event_name == 'pull_request'
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/coverage-summary.json
retention-days: 1
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: '22'
cache: yarn

- name: Instalar dependências
run: yarn install --frozen-lockfile

- name: Testes unitários com cobertura
run: >
yarn testes-unitarios
--coverage
--coverageReporters=json-summary
--coverageReporters=text

- name: Salvar resultados da cobertura
if: github.event_name == 'pull_request'
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/coverage-summary.json
retention-days: 1

e2e:
name: Testes E2E (End-to-End/Ponta a Ponta)
needs: checks
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: '22'
cache: yarn

- name: Instalar dependências
run: yarn install --frozen-lockfile

- name: Instalar navegadores do Playwright
run: npx playwright install --with-deps chromium

- name: Executar testes E2E
run: xvfb-run yarn testar-web
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,9 @@
},
"scripts": {
"compilar": "tsc -p ./",
"lint": "yarn eslint depuracao --ext ts",
"lint": "yarn eslint fontes --ext .ts",
"typecheck": "tsc -p tsconfig.json --noEmit",
"build": "yarn build-base && yarn compilar-web",
"build-base": "esbuild ./fontes/extensao.ts --bundle --sources-content=true --tsconfig=./tsconfig.json --external:vscode --format=cjs --platform=node --outfile=dist/extensao.js",
"build-continuo": "npm run -S build-base -- --sourcemap --watch",
"compilar-web": "webpack --mode development",
Expand All @@ -82,7 +83,7 @@
"testes-unitarios": "jest --coverage",
"testes-unitarios:continuo": "jest --watch",
"testes-unitarios:verboso": "jest --verbose",
"testar-web": "yarn compilar-web && vscode-test-web --browserType=chromium --extensionDevelopmentPath=. .",
"testar-web": "yarn compilar-web && vscode-test-web --browserType=chromium --extensionDevelopmentPath=. --extensionTestsPath=dist/testes/e2e/web/index.js testes/e2e/fixtures",
"vscode:pre-empacotar-web": "yarn pre-empacotar"
},
"activationEvents": [
Expand Down
2 changes: 2 additions & 0 deletions testes/e2e/fixtures/contador.delegua
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
var contador = 1
escreva(contador)
98 changes: 98 additions & 0 deletions testes/e2e/web/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import * as vscode from 'vscode';

function assegurar(condicao: unknown, mensagem: string): asserts condicao {
if (!condicao) {
throw new Error(mensagem);
}
}

async function abrirFixture(): Promise<vscode.TextDocument> {
const pastaWorkspace = vscode.workspace.workspaceFolders?.[0];
assegurar(pastaWorkspace, 'O workspace de fixtures E2E não foi aberto.');

const uri = vscode.Uri.joinPath(pastaWorkspace.uri, 'contador.delegua');
const documento = await vscode.workspace.openTextDocument(uri);
await vscode.window.showTextDocument(documento);
return documento;
}

async function testarAtivacao(): Promise<void> {
const extensao = vscode.extensions.getExtension('designliquido.designliquido-vscode');
assegurar(extensao, 'A extensão Design Líquido não foi encontrada pelo Extension Host.');

await extensao.activate();
assegurar(extensao.isActive, 'A extensão Design Líquido não foi ativada.');
}

async function testarCompletude(documento: vscode.TextDocument): Promise<void> {
const itens = await vscode.commands.executeCommand<vscode.CompletionList>(
'vscode.executeCompletionItemProvider',
documento.uri,
new vscode.Position(1, 8)
);

assegurar(itens?.items.some(item => item.label === 'escreva'),
'A completude da extensão não retornou a função nativa "escreva".');
}

async function testarHover(documento: vscode.TextDocument): Promise<void> {
const hovers = await vscode.commands.executeCommand<vscode.Hover[]>(
'vscode.executeHoverProvider',
documento.uri,
new vscode.Position(1, 2)
);

assegurar(hovers && hovers.length > 0,
'O provedor de Hover não retornou nenhuma informação.');

const possuiConteudo = hovers.some(h =>
h.contents.some(c => {
const texto = typeof c === 'string' ? c : c.value;
return texto.includes('escreva') || texto.includes('função');
})
);

assegurar(possuiConteudo, 'O Hover sobre a função "escreva" não retornou a documentação esperada.');
}

async function testarDefinicao(documento: vscode.TextDocument): Promise<void> {
const definicoes = await vscode.commands.executeCommand<vscode.Location[]>(
'vscode.executeDefinitionProvider',
documento.uri,
new vscode.Position(1, 10)
);

assegurar(definicoes && definicoes.length > 0,
'O provedor de definição não retornou nenhuma localização.');

const primeiraDefinicao = definicoes[0];
assegurar(primeiraDefinicao.range.start.line === 0,
`A definição deveria apontar para a linha 0, mas apontou para a linha ${primeiraDefinicao.range.start.line}.`);
}

async function testarRenomeacao(documento: vscode.TextDocument): Promise<void> {
const edicoes = await vscode.commands.executeCommand<vscode.WorkspaceEdit>(
'vscode.executeDocumentRenameProvider',
documento.uri,
new vscode.Position(1, 10),
'total'
);

assegurar(edicoes, 'O provedor de renomeação não retornou edições.');
const edicoesDocumento = edicoes.get(documento.uri) ?? [];
assegurar(edicoesDocumento.length === 2,
`A renomeação deveria editar duas ocorrências, mas retornou ${edicoesDocumento.length}.`);
assegurar(edicoesDocumento.every(edicao => edicao.newText === 'total'),
'A renomeação não aplicou o novo identificador a todas as ocorrências.');
}

export async function run(): Promise<void> {
await testarAtivacao();
const documento = await abrirFixture();

await testarCompletude(documento);
await testarHover(documento);
await testarDefinicao(documento);
await testarRenomeacao(documento);
console.log('Todos os testes E2E passaram com sucesso!');
}
152 changes: 152 additions & 0 deletions testes/lsp/contrato-delegua-lsp.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// @ts-nocheck
import { beforeEach, describe, expect, it, jest } from '@jest/globals';

import * as lsp from '@designliquido/delegua-lsp';
import * as cacheAnalise from '@designliquido/delegua-lsp/analise/cache-analise';
import * as cacheDefinicoes from '@designliquido/delegua-lsp/analise/cache-definicoes';

import { ambienteVscode } from '../../fontes/ambiente/ambiente-vscode';
import { DeleguaProvedorCompletude } from '../../fontes/completude/delegua-provedor-completude';
import { DeleguaProvedorRenomeacao } from '../../fontes/renomeacao/delegua-provedor-renomeacao';

jest.unmock('@designliquido/delegua-lsp');
jest.unmock('@designliquido/delegua-lsp/analise/cache-analise');
jest.unmock('@designliquido/delegua-lsp/analise/cache-definicoes');

const arquivos = new Map<string, string>();

jest.mock('vscode', () => ({
CompletionItem: class CompletionItem {
constructor(public label: string, public kind?: number) {}
documentation: unknown;
detail: string | undefined;
insertText: unknown;
sortText: string | undefined;
},
MarkdownString: class MarkdownString {
constructor(public value: string) {}
},
Uri: {
parse: (caminho: string) => ({ caminho }),
file: (caminho: string) => ({ caminho }),
},
Range: class Range {
start: { line: number; character: number };
end: { line: number; character: number };

constructor(startLine: number, startCharacter: number, endLine: number, endCharacter: number) {
this.start = { line: startLine, character: startCharacter };
this.end = { line: endLine, character: endCharacter };
}
},
TextEdit: class TextEdit {
constructor(public range: unknown, public newText: string) {}
},
WorkspaceEdit: class WorkspaceEdit {
edicoes = new Map<string, unknown[]>();

set(uri: { caminho: string }, edicoes: unknown[]) {
this.edicoes.set(uri.caminho, edicoes);
}
},
workspace: {
workspaceFolders: [{ uri: { fsPath: '/workspace' } }],
fs: {
readFile: async (uri: { caminho: string }) => {
const conteudo = arquivos.get(uri.caminho);
if (conteudo === undefined) {
throw new Error(`Arquivo não encontrado: ${uri.caminho}`);
}
return new TextEncoder().encode(conteudo);
},
readDirectory: async () => [],
},
},
}), { virtual: true });

function criarDocumento(texto: string) {
return {
uri: { toString: () => 'file:///workspace/teste.delegua' },
fileName: '/workspace/teste.delegua',
getText: () => texto,
version: 1,
languageId: 'delegua',
offsetAt: (posicao: { line: number; character: number }) => texto
.split('\n')
.slice(0, posicao.line)
.reduce((total, linha) => total + linha.length + 1, 0) + posicao.character,
};
}

describe('contrato com @designliquido/delegua-lsp', () => {
beforeEach(() => {
arquivos.clear();
jest.clearAllMocks();
});

describe('verificação do contrato de exports (API pública do LSP)', () => {
it('expõe as capacidades principais usadas pela extensão', () => {
expect(lsp.proverItensCompletude).toEqual(expect.any(Function));
expect(lsp.proverDefinicao).toEqual(expect.any(Function));
expect(lsp.proverReferencias).toEqual(expect.any(Function));
expect(lsp.prepareRename).toEqual(expect.any(Function));
expect(lsp.provideRenameEdits).toEqual(expect.any(Function));
});

it('expõe as funções do módulo de cache de análise', () => {
expect(cacheAnalise.expirarResultado).toEqual(expect.any(Function));
expect(cacheAnalise.expirarResultados).toEqual(expect.any(Function));
expect(cacheAnalise.expirarResultadosPorDependenciaArquivo).toEqual(expect.any(Function));
expect(cacheAnalise.expirarTudo).toEqual(expect.any(Function));
expect(cacheAnalise.obterResultado).toEqual(expect.any(Function));
expect(cacheAnalise.definirResultado).toEqual(expect.any(Function));
expect(cacheAnalise.obterDiagnosticos).toEqual(expect.any(Function));
expect(cacheAnalise.obterResultadoValido).toEqual(expect.any(Function));
});

it('expõe as funções do módulo de cache de definições', () => {
expect(cacheDefinicoes.expirarTodasDefinicoes).toEqual(expect.any(Function));
expect(cacheDefinicoes.definirDefinicoes).toEqual(expect.any(Function));
expect(cacheDefinicoes.obterDefinicoesPorContexto).toEqual(expect.any(Function));
});
});

describe('integração comportamental', () => {
it('fornece itens reais de completude por meio do provedor da extensão', () => {
const provedor = new DeleguaProvedorCompletude();
const itens = provedor.provideCompletionItems(criarDocumento('esc'), { line: 0, character: 3 }, {} as any);

expect(itens).toEqual(expect.arrayContaining([
expect.objectContaining({ label: 'escreva' }),
]));
});

it('gera edições de renomeação reais e as converte para WorkspaceEdit', async () => {
const provedor = new DeleguaProvedorRenomeacao();
const documento = criarDocumento('var contador = 1\nescreva(contador)');

const resultado = await provedor.provideRenameEdits(
documento,
{ line: 1, character: 10 },
'total',
{} as any
);

expect(resultado?.edicoes.get('file:///workspace/teste.delegua')).toHaveLength(2);
expect(resultado?.edicoes.get('file:///workspace/teste.delegua')).toEqual(
expect.arrayContaining([
expect.objectContaining({ newText: 'total' }),
])
);
});

it('disponibiliza ao LSP o sistema de arquivos da API do VS Code', async () => {
arquivos.set('/workspace/biblioteca.delegua', 'var resposta = 42');

await expect(ambienteVscode.sistemaArquivos.lerArquivoTexto('/workspace/biblioteca.delegua'))
.resolves.toBe('var resposta = 42');
await expect(ambienteVscode.sistemaArquivos.lerArquivoTexto('/workspace/inexistente.delegua'))
.resolves.toBeUndefined();
});
});
});
Loading
Loading