Skip to content

Commit d20ad90

Browse files
authored
104: reorganiza configuração do CI e adiciona testes E2E e de contrato (#135)
1 parent 11d2212 commit d20ad90

6 files changed

Lines changed: 337 additions & 28 deletions

File tree

.github/workflows/principal.yml

Lines changed: 75 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,88 @@
1-
name: Testes Unitários
1+
name: Integração Contínua
22

33
on:
44
pull_request:
5-
branches: [ principal ]
5+
branches: [principal]
66
push:
7-
branches: [ principal ]
7+
branches: [principal]
88

99
permissions:
1010
contents: read
1111

1212
jobs:
13+
checks:
14+
name: Verificações
15+
runs-on: ubuntu-latest
16+
17+
steps:
18+
- uses: actions/checkout@v4
19+
20+
- uses: actions/setup-node@v4
21+
with:
22+
node-version: '22'
23+
cache: yarn
24+
25+
- name: Instalar dependências
26+
run: yarn install --frozen-lockfile
27+
28+
- name: Typecheck
29+
run: yarn typecheck
30+
31+
- name: Lint
32+
run: yarn lint
33+
34+
- name: Build
35+
run: yarn build
36+
1337
testes-unitarios:
38+
name: Testes Unitários
39+
needs: checks
1440
runs-on: ubuntu-latest
1541

1642
steps:
17-
- uses: actions/checkout@v4
18-
19-
- uses: actions/setup-node@v4
20-
with:
21-
node-version: '22'
22-
cache: 'yarn'
23-
24-
- name: Yarn - Dependências
25-
run: yarn install --frozen-lockfile
26-
27-
- name: Testes unitários com cobertura
28-
run: yarn testes-unitarios --coverage --coverageReporters=json-summary --coverageReporters=text
29-
30-
- name: Salvar resultados de cobertura
31-
if: github.event_name == 'pull_request'
32-
uses: actions/upload-artifact@v4
33-
with:
34-
name: coverage-report
35-
path: coverage/coverage-summary.json
36-
retention-days: 1
43+
- uses: actions/checkout@v4
44+
45+
- uses: actions/setup-node@v4
46+
with:
47+
node-version: '22'
48+
cache: yarn
49+
50+
- name: Instalar dependências
51+
run: yarn install --frozen-lockfile
52+
53+
- name: Testes unitários com cobertura
54+
run: >
55+
yarn testes-unitarios
56+
--coverage
57+
--coverageReporters=json-summary
58+
--coverageReporters=text
59+
60+
- name: Salvar resultados da cobertura
61+
if: github.event_name == 'pull_request'
62+
uses: actions/upload-artifact@v4
63+
with:
64+
name: coverage-report
65+
path: coverage/coverage-summary.json
66+
retention-days: 1
67+
68+
e2e:
69+
name: Testes E2E (End-to-End/Ponta a Ponta)
70+
needs: checks
71+
runs-on: ubuntu-latest
72+
73+
steps:
74+
- uses: actions/checkout@v4
75+
76+
- uses: actions/setup-node@v4
77+
with:
78+
node-version: '22'
79+
cache: yarn
80+
81+
- name: Instalar dependências
82+
run: yarn install --frozen-lockfile
83+
84+
- name: Instalar navegadores do Playwright
85+
run: npx playwright install --with-deps chromium
86+
87+
- name: Executar testes E2E
88+
run: xvfb-run yarn testar-web

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,9 @@
6666
},
6767
"scripts": {
6868
"compilar": "tsc -p ./",
69-
"lint": "yarn eslint depuracao --ext ts",
69+
"lint": "yarn eslint fontes --ext .ts",
7070
"typecheck": "tsc -p tsconfig.json --noEmit",
71+
"build": "yarn build-base && yarn compilar-web",
7172
"build-base": "esbuild ./fontes/extensao.ts --bundle --sources-content=true --tsconfig=./tsconfig.json --external:vscode --format=cjs --platform=node --outfile=dist/extensao.js",
7273
"build-continuo": "npm run -S build-base -- --sourcemap --watch",
7374
"compilar-web": "webpack --mode development",
@@ -82,7 +83,7 @@
8283
"testes-unitarios": "jest --coverage",
8384
"testes-unitarios:continuo": "jest --watch",
8485
"testes-unitarios:verboso": "jest --verbose",
85-
"testar-web": "yarn compilar-web && vscode-test-web --browserType=chromium --extensionDevelopmentPath=. .",
86+
"testar-web": "yarn compilar-web && vscode-test-web --browserType=chromium --extensionDevelopmentPath=. --extensionTestsPath=dist/testes/e2e/web/index.js testes/e2e/fixtures",
8687
"vscode:pre-empacotar-web": "yarn pre-empacotar"
8788
},
8889
"activationEvents": [
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
var contador = 1
2+
escreva(contador)

testes/e2e/web/index.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import * as vscode from 'vscode';
2+
3+
function assegurar(condicao: unknown, mensagem: string): asserts condicao {
4+
if (!condicao) {
5+
throw new Error(mensagem);
6+
}
7+
}
8+
9+
async function abrirFixture(): Promise<vscode.TextDocument> {
10+
const pastaWorkspace = vscode.workspace.workspaceFolders?.[0];
11+
assegurar(pastaWorkspace, 'O workspace de fixtures E2E não foi aberto.');
12+
13+
const uri = vscode.Uri.joinPath(pastaWorkspace.uri, 'contador.delegua');
14+
const documento = await vscode.workspace.openTextDocument(uri);
15+
await vscode.window.showTextDocument(documento);
16+
return documento;
17+
}
18+
19+
async function testarAtivacao(): Promise<void> {
20+
const extensao = vscode.extensions.getExtension('designliquido.designliquido-vscode');
21+
assegurar(extensao, 'A extensão Design Líquido não foi encontrada pelo Extension Host.');
22+
23+
await extensao.activate();
24+
assegurar(extensao.isActive, 'A extensão Design Líquido não foi ativada.');
25+
}
26+
27+
async function testarCompletude(documento: vscode.TextDocument): Promise<void> {
28+
const itens = await vscode.commands.executeCommand<vscode.CompletionList>(
29+
'vscode.executeCompletionItemProvider',
30+
documento.uri,
31+
new vscode.Position(1, 8)
32+
);
33+
34+
assegurar(itens?.items.some(item => item.label === 'escreva'),
35+
'A completude da extensão não retornou a função nativa "escreva".');
36+
}
37+
38+
async function testarHover(documento: vscode.TextDocument): Promise<void> {
39+
const hovers = await vscode.commands.executeCommand<vscode.Hover[]>(
40+
'vscode.executeHoverProvider',
41+
documento.uri,
42+
new vscode.Position(1, 2)
43+
);
44+
45+
assegurar(hovers && hovers.length > 0,
46+
'O provedor de Hover não retornou nenhuma informação.');
47+
48+
const possuiConteudo = hovers.some(h =>
49+
h.contents.some(c => {
50+
const texto = typeof c === 'string' ? c : c.value;
51+
return texto.includes('escreva') || texto.includes('função');
52+
})
53+
);
54+
55+
assegurar(possuiConteudo, 'O Hover sobre a função "escreva" não retornou a documentação esperada.');
56+
}
57+
58+
async function testarDefinicao(documento: vscode.TextDocument): Promise<void> {
59+
const definicoes = await vscode.commands.executeCommand<vscode.Location[]>(
60+
'vscode.executeDefinitionProvider',
61+
documento.uri,
62+
new vscode.Position(1, 10)
63+
);
64+
65+
assegurar(definicoes && definicoes.length > 0,
66+
'O provedor de definição não retornou nenhuma localização.');
67+
68+
const primeiraDefinicao = definicoes[0];
69+
assegurar(primeiraDefinicao.range.start.line === 0,
70+
`A definição deveria apontar para a linha 0, mas apontou para a linha ${primeiraDefinicao.range.start.line}.`);
71+
}
72+
73+
async function testarRenomeacao(documento: vscode.TextDocument): Promise<void> {
74+
const edicoes = await vscode.commands.executeCommand<vscode.WorkspaceEdit>(
75+
'vscode.executeDocumentRenameProvider',
76+
documento.uri,
77+
new vscode.Position(1, 10),
78+
'total'
79+
);
80+
81+
assegurar(edicoes, 'O provedor de renomeação não retornou edições.');
82+
const edicoesDocumento = edicoes.get(documento.uri) ?? [];
83+
assegurar(edicoesDocumento.length === 2,
84+
`A renomeação deveria editar duas ocorrências, mas retornou ${edicoesDocumento.length}.`);
85+
assegurar(edicoesDocumento.every(edicao => edicao.newText === 'total'),
86+
'A renomeação não aplicou o novo identificador a todas as ocorrências.');
87+
}
88+
89+
export async function run(): Promise<void> {
90+
await testarAtivacao();
91+
const documento = await abrirFixture();
92+
93+
await testarCompletude(documento);
94+
await testarHover(documento);
95+
await testarDefinicao(documento);
96+
await testarRenomeacao(documento);
97+
console.log('Todos os testes E2E passaram com sucesso!');
98+
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
// @ts-nocheck
2+
import { beforeEach, describe, expect, it, jest } from '@jest/globals';
3+
4+
import * as lsp from '@designliquido/delegua-lsp';
5+
import * as cacheAnalise from '@designliquido/delegua-lsp/analise/cache-analise';
6+
import * as cacheDefinicoes from '@designliquido/delegua-lsp/analise/cache-definicoes';
7+
8+
import { ambienteVscode } from '../../fontes/ambiente/ambiente-vscode';
9+
import { DeleguaProvedorCompletude } from '../../fontes/completude/delegua-provedor-completude';
10+
import { DeleguaProvedorRenomeacao } from '../../fontes/renomeacao/delegua-provedor-renomeacao';
11+
12+
jest.unmock('@designliquido/delegua-lsp');
13+
jest.unmock('@designliquido/delegua-lsp/analise/cache-analise');
14+
jest.unmock('@designliquido/delegua-lsp/analise/cache-definicoes');
15+
16+
const arquivos = new Map<string, string>();
17+
18+
jest.mock('vscode', () => ({
19+
CompletionItem: class CompletionItem {
20+
constructor(public label: string, public kind?: number) {}
21+
documentation: unknown;
22+
detail: string | undefined;
23+
insertText: unknown;
24+
sortText: string | undefined;
25+
},
26+
MarkdownString: class MarkdownString {
27+
constructor(public value: string) {}
28+
},
29+
Uri: {
30+
parse: (caminho: string) => ({ caminho }),
31+
file: (caminho: string) => ({ caminho }),
32+
},
33+
Range: class Range {
34+
start: { line: number; character: number };
35+
end: { line: number; character: number };
36+
37+
constructor(startLine: number, startCharacter: number, endLine: number, endCharacter: number) {
38+
this.start = { line: startLine, character: startCharacter };
39+
this.end = { line: endLine, character: endCharacter };
40+
}
41+
},
42+
TextEdit: class TextEdit {
43+
constructor(public range: unknown, public newText: string) {}
44+
},
45+
WorkspaceEdit: class WorkspaceEdit {
46+
edicoes = new Map<string, unknown[]>();
47+
48+
set(uri: { caminho: string }, edicoes: unknown[]) {
49+
this.edicoes.set(uri.caminho, edicoes);
50+
}
51+
},
52+
workspace: {
53+
workspaceFolders: [{ uri: { fsPath: '/workspace' } }],
54+
fs: {
55+
readFile: async (uri: { caminho: string }) => {
56+
const conteudo = arquivos.get(uri.caminho);
57+
if (conteudo === undefined) {
58+
throw new Error(`Arquivo não encontrado: ${uri.caminho}`);
59+
}
60+
return new TextEncoder().encode(conteudo);
61+
},
62+
readDirectory: async () => [],
63+
},
64+
},
65+
}), { virtual: true });
66+
67+
function criarDocumento(texto: string) {
68+
return {
69+
uri: { toString: () => 'file:///workspace/teste.delegua' },
70+
fileName: '/workspace/teste.delegua',
71+
getText: () => texto,
72+
version: 1,
73+
languageId: 'delegua',
74+
offsetAt: (posicao: { line: number; character: number }) => texto
75+
.split('\n')
76+
.slice(0, posicao.line)
77+
.reduce((total, linha) => total + linha.length + 1, 0) + posicao.character,
78+
};
79+
}
80+
81+
describe('contrato com @designliquido/delegua-lsp', () => {
82+
beforeEach(() => {
83+
arquivos.clear();
84+
jest.clearAllMocks();
85+
});
86+
87+
describe('verificação do contrato de exports (API pública do LSP)', () => {
88+
it('expõe as capacidades principais usadas pela extensão', () => {
89+
expect(lsp.proverItensCompletude).toEqual(expect.any(Function));
90+
expect(lsp.proverDefinicao).toEqual(expect.any(Function));
91+
expect(lsp.proverReferencias).toEqual(expect.any(Function));
92+
expect(lsp.prepareRename).toEqual(expect.any(Function));
93+
expect(lsp.provideRenameEdits).toEqual(expect.any(Function));
94+
});
95+
96+
it('expõe as funções do módulo de cache de análise', () => {
97+
expect(cacheAnalise.expirarResultado).toEqual(expect.any(Function));
98+
expect(cacheAnalise.expirarResultados).toEqual(expect.any(Function));
99+
expect(cacheAnalise.expirarResultadosPorDependenciaArquivo).toEqual(expect.any(Function));
100+
expect(cacheAnalise.expirarTudo).toEqual(expect.any(Function));
101+
expect(cacheAnalise.obterResultado).toEqual(expect.any(Function));
102+
expect(cacheAnalise.definirResultado).toEqual(expect.any(Function));
103+
expect(cacheAnalise.obterDiagnosticos).toEqual(expect.any(Function));
104+
expect(cacheAnalise.obterResultadoValido).toEqual(expect.any(Function));
105+
});
106+
107+
it('expõe as funções do módulo de cache de definições', () => {
108+
expect(cacheDefinicoes.expirarTodasDefinicoes).toEqual(expect.any(Function));
109+
expect(cacheDefinicoes.definirDefinicoes).toEqual(expect.any(Function));
110+
expect(cacheDefinicoes.obterDefinicoesPorContexto).toEqual(expect.any(Function));
111+
});
112+
});
113+
114+
describe('integração comportamental', () => {
115+
it('fornece itens reais de completude por meio do provedor da extensão', () => {
116+
const provedor = new DeleguaProvedorCompletude();
117+
const itens = provedor.provideCompletionItems(criarDocumento('esc'), { line: 0, character: 3 }, {} as any);
118+
119+
expect(itens).toEqual(expect.arrayContaining([
120+
expect.objectContaining({ label: 'escreva' }),
121+
]));
122+
});
123+
124+
it('gera edições de renomeação reais e as converte para WorkspaceEdit', async () => {
125+
const provedor = new DeleguaProvedorRenomeacao();
126+
const documento = criarDocumento('var contador = 1\nescreva(contador)');
127+
128+
const resultado = await provedor.provideRenameEdits(
129+
documento,
130+
{ line: 1, character: 10 },
131+
'total',
132+
{} as any
133+
);
134+
135+
expect(resultado?.edicoes.get('file:///workspace/teste.delegua')).toHaveLength(2);
136+
expect(resultado?.edicoes.get('file:///workspace/teste.delegua')).toEqual(
137+
expect.arrayContaining([
138+
expect.objectContaining({ newText: 'total' }),
139+
])
140+
);
141+
});
142+
143+
it('disponibiliza ao LSP o sistema de arquivos da API do VS Code', async () => {
144+
arquivos.set('/workspace/biblioteca.delegua', 'var resposta = 42');
145+
146+
await expect(ambienteVscode.sistemaArquivos.lerArquivoTexto('/workspace/biblioteca.delegua'))
147+
.resolves.toBe('var resposta = 42');
148+
await expect(ambienteVscode.sistemaArquivos.lerArquivoTexto('/workspace/inexistente.delegua'))
149+
.resolves.toBeUndefined();
150+
});
151+
});
152+
});

0 commit comments

Comments
 (0)