Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
d47c045
Adicionada nova funcionalidade X
robertaGreenlegis Mar 8, 2025
f9e12bf
1º e 2º casos de teste, criação de datatest cookes
robertaGreenlegis Mar 9, 2025
561c825
[UsuariosV2] Ajuste nos data-test da tabela
robertaGreenlegis Mar 11, 2025
1447a47
criação de data-test p table e incremento no testes de API
robertaGreenlegis Mar 14, 2025
b92e6a3
Ultimas alterações
robertaGreenlegis Mar 15, 2025
847262d
Adicionando a pasta fixtures ao .gitignore
robertaGreenlegis Mar 16, 2025
cc735b7
Atualizando Cypress para a versão 14.2.0
robertaGreenlegis Mar 16, 2025
fd1160f
Colocando coamndo para rodar aquivo no lugar correto/Edit nome Descri…
robertaGreenlegis Mar 16, 2025
182f424
Adicionado arquivo lixo ao gitignore
robertaGreenlegis Mar 16, 2025
b1c8865
Adcionando maneira correta para rodar os testes no cypress
robertaGreenlegis Mar 16, 2025
3e41942
Adcionando setCookie button de cookie
robertaGreenlegis Mar 16, 2025
3eb4180
Teste Api-Recupera 10 clientes com sucesso e verifica o código de sta…
robertaGreenlegis Mar 16, 2025
ba028c9
Teste Api-Recupera 10 clientes com sucesso e verifica o código de sta…
robertaGreenlegis Mar 16, 2025
f1f39ce
Realiza a paginação de clientes corretamente
robertaGreenlegis Mar 16, 2025
b7de326
Subindo testes de API e também GUI
robertaGreenlegis Mar 21, 2025
2674fdd
Subindo testes de API e também GUI
robertaGreenlegis Mar 21, 2025
61ed3d0
Subindo testes de API e também GUI
robertaGreenlegis Mar 21, 2025
2609361
Remove a pasta Lixo do repositório e adiciona ao .gitignore
robertaGreenlegis Mar 21, 2025
8d2dba9
Remove a pasta Lixo do repositório e adiciona ao .gitignore
robertaGreenlegis Mar 21, 2025
d98f0ae
Ajustes sugeridos pelo gitcopilot
robertaGreenlegis Mar 21, 2025
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
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,11 @@ cypress/downloads/
cypress/screenshots/
cypress/videos/


Comment thread
rob364 marked this conversation as resolved.
# misc
.DS_Store
cypress/fixtures/
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As pastas cypress/fixtures/ e cypress/support/ não devem ser listadas no arquivo .gitignore.

Em vez disso, se você não vai as utilizar, deve atualizar o arquivo de configurações com as opções fixtureFolder: fase e supportFile: false, deletá-las e mais nada.

/Lixo
Comment thread
rob364 marked this conversation as resolved.
Comment thread
rob364 marked this conversation as resolved.
/bash.exe.stackdump
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

E esse arquivo?

cypress/fixtures/
Comment thread
rob364 marked this conversation as resolved.
cypress/support/
Comment thread
rob364 marked this conversation as resolved.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ Read the following [doc](./docs/TestEnvironment.md) to install and start the bac

Read the following [doc](./docs/TestCases.md) to get a list of test cases.

to run the test cases:
npx cypress open
Comment thread
rob364 marked this conversation as resolved.
Copy link
Copy Markdown
Owner

@wlsf82 wlsf82 Mar 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recomendo assistir a live Lições de Markdown para ótimas documentações.

Utilizar markdown efetivamente é um diferencial na escrita de documentação de projetos de software.

Quando há trechos de código na documentação, use crase entre tal trecho para que ele seja renderizado como código.

Veja abaixo:

npx cypress open.

A propósito, em vez de sugerir a execução do comando npx cypress open, recomendo criar um script no arquivo package.json que execute o comando cypress open, e na documentação, referenciar tal script.

Algo assim:

"cy:open": "cypress open"

E na documentação, ficaria algo assim:

To run the tests, run npm run cy:open.


___

Made with ❤️ by [Walmyr](https://walmyr.dev).



Comment thread
rob364 marked this conversation as resolved.
11 changes: 11 additions & 0 deletions cypress.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const { defineConfig } = require("cypress");

module.exports = defineConfig({
e2e: {
baseUrl: "http://localhost:3000",
env: {
apiUrl: "http://localhost:3001",
CUSTOMERS_API_URL: "http://localhost:3001/customers",
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Você está repetindo a string da apiUrl aqui.

Basta a apiUrl aqui e no arquivo dos testes de API você criaria uma variável chamada CUSTOMERS_API_URL, a qual armazenaria o seguinte valor: ${Cypress.env('apiUrl')}/customers.

},
},
});
37 changes: 37 additions & 0 deletions cypress/e2e/Api.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
describe("EngageSphere API", () => {
const apiUrl = Cypress.env("CUSTOMERS_API_URL");
Comment thread
rob364 marked this conversation as resolved.
it("Verificando se na recuperação do cliente o status é 200", () => {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Que tal uma descrição mais simples, como: "retorna o código de status 200 ao buscar clientes"?

const page = 1;
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Não há necessidade de pagar a página 1 visto que este já é o valor padrão da API se a query string page não é passada.


cy.request({
method: "GET",
url: `${apiUrl}?page=${page}`,
}).then((response) => {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recomendo usar desestruturação de objetos aqui.

É comum alunos/as testarem APIs e repetirem response. ao longo das verificações de resultado esperado.

Exemplo:

it('handles invalid requests gracefully (e.g., negative page)', () => {
  cy.request({
    method: 'GET',
    url: `${CUSTOMERS_API_URL}?page=-1`,
    failOnStatusCode: false,
  }).then((response) => {
    expect(response.status).to.eq(400)
    expect(response.body.error).to.include('Invalid page or limit. Both must be positive numbers.')
  })
})
 
it('handles invalid requests gracefully (e.g., negative limit)', () => {
  cy.request({
    method: 'GET',
    url: `${CUSTOMERS_API_URL}?limit=-1`,
    failOnStatusCode: false,
  }).then((response) => {
    expect(response.status).to.eq(400)
    expect(response.body.error).to.include('Invalid page or limit. Both must be positive numbers.')
  })
})

Por que tal prática fere um bom design de testes?

Testes automatizados devem ser o mais objetivos e diretos ao ponto quanto possível. Além disso, código repetido atrapalha a leitura e entendimento do mesmo.

Sugestão com um melhor design

Utilize desestruturação de objetos.

Para aprender mais sobre o assunto, assista a seguinte live que fiz no Canal TAT no YouTube: JavaScript para QA #8 - Atribuição via Desestruturação

Exemplo:

it('handles invalid requests gracefully (e.g., negative page)', () => {
  cy.request({
    method: 'GET',
    url: `${CUSTOMERS_API_URL}?page=-1`,
    failOnStatusCode: false,
  }).then(({ status, body }) => {
    expect(status).to.eq(400)
    expect(body.error).to.include('Invalid page or limit. Both must be positive numbers.')
  })
})

it('handles invalid requests gracefully (e.g., negative limit)', () => {
  cy.request({
    method: 'GET',
    url: `${CUSTOMERS_API_URL}?limit=-1`,
    failOnStatusCode: false,
  }).then(({ status, body }) => {
    expect(status).to.eq(400)
    expect(body.error).to.include('Invalid page or limit. Both must be positive numbers.')
  })
})

expect(response.status).to.eq(200);
});
});

it("Recuperando 10 clientes com sucesso", () => {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uma sugestão melhor para a descrição do caso de testes seria:

Suggested change
it("Recuperando 10 clientes com sucesso", () => {
it("retorna 10 clientes quando limit é iqual a 10", () => {

const page = 1;
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mesmo que já escrevi em outro comentário.


cy.request({
method: "GET",
url: `${apiUrl}?page=${page}`,
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

E aqui, você passaria limit=10, ou, simplismente deixaria a URL da API, visto que o padrão já é limit=10.

}).then((response) => {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recomendo usar desestruturação de objetos conforme explicado anteriormente, para não precisar repetir response..

expect(response.body).to.have.property("customers");
expect(response.body.customers).to.be.an("array").that.has.length(10);
});
});

it("Realiza a paginação de clientes corretamente", () => {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Que tal a seguinte descrição?

Suggested change
it("Realiza a paginação de clientes corretamente", () => {
it("retorna clientes da página 2", () => {

cy.request("GET", `${apiUrl}?page=2`).as("getCustomersPageTwo");

cy.get("@getCustomersPageTwo").then((response) => {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aqui também dá pra usar desestruturação de objetos para evitar repetir response. nas linhas abaixo.

expect(response.body).to.have.property("pageInfo");
expect(response.body.pageInfo).to.have.property("currentPage", 2);
});
});
});



63 changes: 63 additions & 0 deletions cypress/e2e/Gui.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
describe("EngageSphere GUI", () => {
beforeEach(() => {
cy.setCookie('cookieConsent', 'accepted');
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👏🏻

cy.visit("/");
});
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
});
});

Deixe linhas em branco entre o bloco beforeEach e o bloco it para facilitar diferenciar que acabou um e começou outro.

Veja um exemplo abaixo:

describe('EngageSphere', () => {
  beforeEach(() => {
    cy.visit('/')
  })

  it('shows the default greeting (i.e., Hi, there...)', () => {
    // Test implementation here
  })

  it('shows the customized greeting (i.e., Hi, Joe...)', () => {
    // Test implementation here
  })

  // Outros casos de teste aqui...
})

it("Recupera clientes com sucesso e mantém os filtros ao voltar da visualização de detalhes do cliente *)", () => {
cy.get('[data-testid="size-filter"]').select('Small');
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Separa as pré-condições, ações e resultado esperado dos testes por uma linha em branco.

Dessa forma, é possível facilmente identificar as diferentes partes do mesmo.

Assim:

// Pré-condição
cy.get('[data-testid="size-filter"]').select('Small');
cy.contains('button', 'View').click();

// Ação
cy.contains('button', 'Back').click();

// Resultado esperado
cy.get('[data-testid="size-filter"]').should('have.value', 'Small');

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

O mesmo vale para qualquer outro caso de teste onde tal padrão não esteja sendo seguido.

cy.contains('button', 'View').click();
cy.contains('button', 'Back').click();
cy.get('[data-testid="size-filter"]').should('have.value', 'Small');
});
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
});
});

it('exibe o rodapé com o texto e links corretos', () => {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Veja uma opção simplificada, sem a necessidade do uso de .forEach.

cy.contains('p', 'Copyright 2025 - Talking About Testing')
  .should('be.visible')
cy.contains('a', 'Podcast')
  .should('be.visible')
  .and('have.attr', 'href', 'https://open.spotify.com/show/5HFlqWkk6qtgJquUixyuKo')
  .and('have.attr', 'target', '_blank')
cy.contains('a', 'Courses')
  .should('be.visible')
  .and('have.attr', 'href', 'https://talking-about-testing.vercel.app/')
  .and('have.attr', 'target', '_blank')
cy.contains('a', 'Blog')
  .should('be.visible')
  .and('have.attr', 'href', 'https://talkingabouttesting.com')
  .and('have.attr', 'target', '_blank')
cy.contains('a', 'YouTube')
  .should('be.visible')
  .and('have.attr', 'href', 'https://youtube.com/@talkingabouttesting')
  .and('have.attr', 'target', '_blank')

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A propósito, é extremamente importante verificar pela visibilidade dos links.

cy.get('[data-testid="footer"]').should('be.visible')
cy.get('[data-testid="footer"]').contains('Copyright 2025 - Talking About Testing')
// Verifica os links
const links = [
{ text: 'Blog', href: 'https://talkingabouttesting.com' },
{ text: 'Courses', href: 'https://talking-about-testing.vercel.app/' },
{ text: 'Podcast', href: 'https://open.spotify.com/show/5HFlqWkk6qtgJquUixyuKo' },
{ text: 'YouTube', href: 'https://youtube.com/@talkingabouttesting' }
]
links.forEach(link => {
cy.get('[data-testid="footer"]')
.contains('a', link.text)
.should('have.attr', 'href', link.href)
.and('have.attr', 'target', '_blank') // Garante que o link abre em nova aba
})
})
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
})
})

it('exibe a saudação padrão "Hi there!" quando nenhum nome é fornecido', () => {
cy.get('[data-testid="name"]').type('Joe')
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aqui também deveria separar ação e resultado esperado por uma linha em branco.

Suggested change
cy.get('[data-testid="name"]').type('Joe')
cy.get('[data-testid="name"]').type('Joe')

cy.get('[data-testid="table"]')
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

E essas três linhas de código poderiam ser uma única, se você fizesse assim:

cy.contains('h2', 'Hi Joe').should('be.visible')

A propósito, aqui é importante também verificar pela visibilidade do elemento, pois ele pode estar no DOM e não estar visível.

.find('h2')
.should('contain.text', 'Hi Joe')
})
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
})
})

it("Retorna à lista de clientes ao clicar no botão Voltar", () => {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Veja uma opção que separa pré-condição, ação e resultado esperado.

it("Retorna à lista de clientes ao clicar no botão Voltar", () => {
  // Pré-condição
  cy.contains('button', 'View').click();

  // Ação
  cy.contains('button', 'Back').click();

  // Resultado esperado
  cy.get('table').should('be.visible')
});

cy.contains('button', 'View').click();
cy.contains('button', 'Back').click();
cy.url().should("eq", "http://localhost:3000/");
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A URL nunca muda, ou seja, essa verificação funcionaria também nos detalhes do cliente.

Em vez disso, verifique que a tabela de clientes agora está visível, conforme o exemplo que deixei acima.


});
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
});
});

describe("Filtragem por tamanho", () => {
const filtros = ['Small', 'Medium', 'Enterprise', 'Large Enterprise', 'Very Large Enterprise'];
filtros.forEach((filtro) => {
it(`Deve filtrar por ${filtro} e garantir que o filtro permanece ao voltar`, () => {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Já há um teste bem no início para testar tal comportamento.

Por qual motivo testar isso de novo?

cy.get('[data-testid="size-filter"]').select(filtro);
cy.contains('button', 'View').click();
cy.contains('button', 'Back').click();
cy.get('[data-testid="size-filter"]').should('have.value', filtro);
});
});
});
describe("Filtragem por indústria", () => {
const industrias = ['Logistics', 'Retail', 'Technology', 'HR', 'Finance'];
industrias.forEach((industria) => {
it(`Deve filtrar por ${industria} e garantir que o filtro permanece ao voltar`, () => {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Um único teste bastaria, não precisa um para cada indústria.

cy.get('[data-testid="industry-filter"]').select(industria);
cy.contains('button', 'View').click();
cy.contains('button', 'Back').click();
cy.get('[data-testid="industry-filter"]').should('have.value', industria);
});
});
});
});
5 changes: 5 additions & 0 deletions cypress/fixtures/example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Não recomenda-se deixar as pastas cypress/fixtures/ no projeto quando esta não está em uso.

Por que tal prática fere um bom design de testes?

Tal prática aumenta a complexidade do projeto visto que é mais código para ler e entender.

Sugestão com um melhor design

Caso a pasta citada acima não esteja em uso, é recomendado deletar tal pasta por completo e configurar o Cypress para ignorá-la, conforme demonstrado abaixo.

const { defineConfig } = require('cypress')

module.exports = defineConfig({
  e2e: {
    baseUrl: '...',
    fixturesFolder: false,
  },
})

Dessa forma, ao inicializar o Cypress, o mesmo não irá re-gerar tal pasta, a base
de código estará menor e mais simples, que é exatamente o que buscamos com um bom
design (simplicidade).

"name": "Using fixtures to represent data",
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Faltou deletar o arquivo.

"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}
Empty file added cypress/support/commands.js
Empty file.
1 change: 1 addition & 0 deletions cypress/support/e2e.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import './commands'
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Faltou deletar este arquivo também.

2 changes: 1 addition & 1 deletion frontend/src/components/CustomerDetails/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const CustomerDetails = ({ customer, onClick }) => {
<button className={styles.showAddressBtn} onClick={showAddressHandler}>Show address</button>
</div>
)}
<Button text="Back" onClick={onClick} />
<Button text="Back" onClick={onClick} datatest="button-back" />
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Esse datatest não é necessário, visto que é possível selecionar o botão com o comando cy.contains('button', 'Back').

</div>
)
}
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/Greeting/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ const Greeting = ({ name }) => {
}

export default Greeting

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

E estas linhas em branco?


102 changes: 100 additions & 2 deletions frontend/src/components/Table/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@ const Table = ({
<td>{customer.industry}</td>
<td>{customer.employees}</td>
<td>{customer.size}</td>
<td onClick={() => customerActionClickHandler(customer)}>
<td onClick={() => customerActionClickHandler(customer)} >
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Qual o motivo dessa mudança?

<strong>
<button aria-label={`View company: ${customer.name}`} key={customer.id} onClick={() => customerActionClickHandler(customer)}>
<button aria-label={`View company: ${customer.name}`} key={customer.id} onClick={() => customerActionClickHandler(customer)} >
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

E dessa?

View
</button>
</strong>
Expand All @@ -82,3 +82,101 @@ const Table = ({
}

export default Table

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remover esses códigos comentados



// import { useState } from 'react'
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

E esse monte de código do frontend comentado?


// import styles from './Table.module.css'

// const Table = ({
// customers,
// customerActionClickHandler,
// }) => {
// const [sortCriteria, setSortCriteria] = useState('size')
// const [sortOrder, setSortOrder] = useState('desc')

// const sortCustomers = (criteria) => {
// if (sortCriteria === criteria) {
// setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')
// } else {
// setSortOrder('desc')
// }
// setSortCriteria(criteria)
// }

// const sortedCustomers = [...customers].sort((a, b) => {
// const order = sortOrder === 'asc' ? 1 : -1
// if (sortCriteria === 'size') {
// const mapSizeToNumber = (size) => {
// switch (size.toLowerCase()) {
// case 'small': return 1
// case 'medium': return 2
// case 'enterprise': return 3
// case 'large enterprise': return 4
// case 'very large enterprise': return 5
// default: return 0
// }
// }
// return order * (mapSizeToNumber(a[sortCriteria]) - mapSizeToNumber(b[sortCriteria]))
// }
// return order * (a[sortCriteria] - b[sortCriteria])
// })

// const sortNumberOfEmployeesHandler = () => sortCustomers('employees')
// const sortSizeHandler = () => sortCustomers('size')

// return (
// <table border="1" className={styles.container} data-test="customer-table">
// <thead>
// <tr data-test="customer-table-header">
// <th scope="col">ID</th>
// <th scope="col">Company name</th>
// <th scope="col">Industry</th>
// <th scope="col" onClick={sortNumberOfEmployeesHandler} data-test="sort-employees-header">
// <button onClick={sortNumberOfEmployeesHandler} data-test="sort-employees-button">
// Number of employees
// {sortCriteria === 'employees' && (sortOrder === 'asc' ? <span aria-label="ordering by number of employees asc">&uarr;</span> : <span aria-label="ordering by number of employees desc">&darr;</span>)}
// </button>
// </th>
// <th scope="col" onClick={sortSizeHandler} data-test="sort-size-header">
// <button onClick={sortSizeHandler} data-test="sort-size-button">
// Size
// {sortCriteria === 'size' && (sortOrder === 'asc' ? <span aria-label="ordering by size asc">&uarr;</span> : <span aria-label="ordering by size desc">&darr;</span>)}
// </button>
// </th>
// <th scope="col">Action</th>
// </tr>
// </thead>
// <tbody>
// {sortedCustomers.map((customer, index) => (
// <tr key={customer.id} data-test={`customer-row-${index}`}>
// <td data-test={`customer-id-${index}`}>{customer.id}</td>
// <td data-test={`customer-name-${index}`}>{customer.name}</td>
// <td data-test={`customer-industry-${index}`}>{customer.industry}</td>
// <td data-test={`customer-employees-${index}`}>{customer.employees}</td>
// <td data-test={`customer-size-${index}`}>{customer.size}</td>
// <td onClick={() => customerActionClickHandler(customer)} data-test={`customer-action-${index}`}>
// <strong>
// <button
// aria-label={`View company: ${customer.name}`}
// key={customer.id}
// onClick={() => customerActionClickHandler(customer)}
// data-test={`view-customer-${index}`}
// >
// View
// </button>
// </strong>
// </td>
// </tr>
// ))}
// </tbody>
// </table>

// )
// }

// export default Table



Loading