Compare commits

...

10 Commits

Author SHA1 Message Date
5beab5f38d ignore log.json 2022-06-11 19:00:59 +09:00
b40bbbca13 space 2022-06-11 19:00:38 +09:00
7afe441f8c new testcase 2022-06-11 18:58:48 +09:00
0fc7294905 feat: now token can be undefined 2022-06-11 18:58:40 +09:00
d2cf8bdea4 test table 2022-06-11 18:39:50 +09:00
2d393515c4 gen doc for functional test 2022-06-11 17:20:41 +09:00
918a4eeb52 reindexing 2022-06-11 16:58:50 +09:00
ce2921a6c1 extract testcase data 2022-06-11 16:58:08 +09:00
7f2471103b Extend Purpose 2022-06-11 16:28:45 +09:00
384c33e6a6 Index 2022-06-11 15:47:16 +09:00
16 changed files with 4186 additions and 648 deletions

2
.gitignore vendored
View File

@ -1,4 +1,4 @@
book
.env
cache
.DS_Store
log.json

View File

@ -3,7 +3,7 @@ authors = ["monoid"]
language = "ko"
multilingual = false
src = "src"
title = "Software Requirement Specification"
title = "Software Development Comprehensive Document"
[preprocessor]
[preprocessor.mermaid]

3434
cache/issues.json vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -1,8 +1,10 @@
# Summary
[Index](./index.md)
- [Introduction](./intro.md)
- [Overall Description](./overall.md)
- [Specific Requirement](./specific.md)
- [Supporting information](./support.md)
- [Requirement Specification Supporting Information](./support.md)
- [Architecture](./architecture.md)
- [Testing](./testing.md)

View File

@ -1,4 +1,4 @@
# 5. 설계
# 5. 설계(Architecture)
## 5.1 UML
@ -398,15 +398,15 @@ FsManager ..> "1" RPCMessageManager
FsManager ..> "1" FsGetResult
```
## 5.2 의사코드
## 5.2 의사코드(Pseudo Code)
의사코드는 다음과 같이 진행된다.
### 서버 RPC 메세지 처리
### 5.2.1 서버 RPC 메세지 처리
클라이언트는 RPC를 진행하기 위해 웹소켓을 연결합니다. 웹소켓의 주소 `/ws`
도달하기 위해서 먼저 라우팅이 진행이 됩니다. 라우팅은 `TreeRouter` 클래스에서
진행됩니다.
클라이언트는 RPC를 진행하기 위해 웹소켓을 연결다. 웹소켓의 주소 `/ws`
도달하기 위해서 먼저 라우팅이 진행이 다. 라우팅은 `TreeRouter` 클래스에서
진행다.
```
Input: req Request
Output: Response
@ -419,7 +419,8 @@ fn Route(req){
response with 404 Not Found
}
```
마침내 엔드포인트에 도달하게 되면 웹소켓을 얻어내고 새로운 연결을 등록합니다.
마침내 엔드포인트에 도달하게 되면 웹소켓을 얻어내고 새로운 연결을 등록한다.
여기서 reqeust session으로 유저를 얻어내서 접속하는 것에 대해 주의하라.
```
Input: req Request
Output: Response
@ -431,8 +432,8 @@ fn RPCHandleEndpoint(req){
res
}
```
이제부터 메세지를 받을 수 있습니다.
메세지가 오면 이 함수가 실행이 됩니다.
이제부터 `RPCMethod` 메세지를 받을 수 있다.
메세지가 오면 함수 `handleMessage` 가 실행이 호출된다.
```
Input: message on send
Output: response
@ -443,11 +444,11 @@ fn Connection.handleMessage(msg: string){
}
```
디스패치가 성공적으로 이루어지면 RPC 작업를 처리하는 함수에 도착합니다.
청크 작업을 예로 들겠습니다. 청크 작업에서는 권한을 확인하고
명령의 충돌을 히스토리를 비교하며 다시 적용하며 해결합니다.
디스패치가 성공적으로 이루어지면 RPC 작업를 처리하는 함수에 도착다.
청크 작업을 예로 들면 이렇다. 청크 작업에서는 권한을 확인하고
명령의 충돌을 히스토리를 비교하며 다시 적용하며 해결다.
그리고 요구된 작업을 처리하고 문서의 `updatedAt`을 업데이트하고
문서 업데이트 사실을 이 문서를 보고 있던 참여자에게 전파합니다.
문서 업데이트 사실을 이 문서를 보고 있던 참여자에게 전파다.
```
Input: conn Connection
Input: m RPCChunkMethod
@ -481,15 +482,15 @@ fn ChunkOperation(conn, m){
}
```
문서 작업도 마찬가지로 이루어집니다.
문서 작업도 마찬가지로 이루어다.
### 클라이언트의 메세지 처리 동기화
### 5.2.2 클라이언트의 메세지 처리 동기화
클라이언트에서는 다음과 같은 일이 일어납니다.
클라이언트에서는 다음과 같은 일이 일어다.
먼저 `notification`을 받습니다. 그러면 모든 `DocumentViewModel`에게 이벤트를 전달합니다.
먼저 `notification`을 받는다. 그러면 모든 `DocumentViewModel`에게 이벤트를 전달한다.
그리고 각각의 `DocumentViewModel`은 자기 문서에 일어난 일인지 확인하고 `ChunkListMutator`를 만들어서
문서에 적용합니다.
문서에 적용다.
```
Input: e RPCNotification
Input: this document view model
@ -502,7 +503,7 @@ fn updateOnNotification(this,notification){
}
```
문서의 레디큐에 mutator를 집어넣고 `seq` 번호가 기다리는 것이면 실행하고 업데이트합니다.
문서의 레디큐에 mutator를 집어넣고 `seq` 번호가 기다리는 것이면 실행하고 업데이트다.
```
Input: mutator ChunkListMutator
@ -531,7 +532,7 @@ fn apply(this, mutator, updatedAt, seq){
}
```
### 다른 작업들
### 5.2.3 다른 작업들
```
module chunk {
@ -550,7 +551,8 @@ module chunk {
}
}
chunkViewer(chunk : Chunk, focusedChunk : State<string>, deleteThis : () => void) : Component {
chunkViewer(chunk : Chunk, focusedChunk : State<string>,
deleteThis : () => void) : Component {
var mode = Read
var c = new Component(
@ -665,7 +667,7 @@ module settings {
return c
}
}
```
```
module frontend {
main() : Component {

52
src/index.md Normal file
View File

@ -0,0 +1,52 @@
# 목차(Index)
1. [소개(Introduction)](./intro.md)
1. [목적(Purpose)](./intro.md#11-목적purpose)
2. [범위(scope)](./intro.md#12-범위scope)
3. [용어 및 약어 정의(Definitions, acronyms and abbreviations)](./intro.md#13-용어-및-약어-정의definitions-acronyms-and-abbreviations)
4. [참고자료(References)](./intro.md#14-참고자료references)
5. [개요(Overview)](./intro.md#15-개요overview)
2. [전체 시스템 개요(Overall description)](./overall.md)
1. [제품 관점(Product perspective)](./overall.md#21-제품-관점product-perspective)
1. [시스템 인터페이스(System interfaces)](./overall.md#211-시스템-인터페이스system-interfaces)
2. [사용자 인터페이스(User interfaces)](./overall.md#212-사용자-인터페이스user-interfaces)
3. [하드웨어 인터페이스(Hardware interfaces)](./overall.md#213-하드웨어-인터페이스hardware-interfaces)
4. [소프트웨어 인터페이스(Software interfaces)](./overall.md#214-소프트웨어-인터페이스software-interfaces)
5. [통신 인터페이스(Communications interfaces)](./overall.md#215-통신-인터페이스communications-interfaces)
6. [메모리 제약사항(Memory constraints)](./overall.md#216-메모리-제약사항memory-constraints)
7. [운영(Operations)](./overall.md#217-운영operations)
8. [사이트 적용 요건(Site adaption requirements)](./overall.md#218-사이트-적용-요건site-adaption-requirements)
2. [제품 기능(Product functions)](./overall.md#22-제품-기능product-functions)
<%
const table = it.table;
let index = 1;
for (const [c,issues] of table) {
const name = `${c} Operation`;
const href = `2.2.${index} ${c} Operation`
%><%= `${index++}. [${name}](./overall.md#${it.toHeadId(href)})\n ` %><%}%><%="\n"%>
3. [상세요구사항(Specific Requirements)](./specific.md)
1. [외부 인터페이스 요구사항(External interface requirements)](./specific.md#31-외부-인터페이스-요구사항external-interface-requirements)
2. [기능 요구사항(Functional requirements)](./specific.md#32-기능-요구사항functional-requirements)
<%= it.issues.map((i)=>`(#${i.number}) ${i.title}`).map(
x=>`* [${x}](./specific.md#${it.toHeadId(x)})`
).join("\n ") %><%= "\n"%>
3. [성능 요구사항(Performance requirements)](./specific.md#33-성능-요구사항performance-requirements)
4. [논리적 데이터베이스 요구사항(Logical database requirements)](./specific.md#34-논리적-데이터베이스-요구사항logical-database-requirements)
5. [설계 제약사항(Design constraints)](./specific.md#35-설계-제약사항design-constraints)
1. [표준 준수(Standards compliance)](./specific.md#351-표준-준수standards-compliance)
6. [소프트웨어 시스템 속성(Software system attributes)](./specific.md#36-소프트웨어-시스템-속성software-system-attributes)
7. [상세 요구사항의 구성(Organizing the specific requirements)](./specific.md#37-상세-요구사항의-구성organizing-the-specific-requirements)
1. [객체(Objects)](./specific.md#371-객체objects)
2. [사용자 인터페이스 상세](./specific.md#372-사용자-인터페이스-상세)
4. [요구사항 명세 추가 이력 (Requirement Specification Supporting Information)](./support.md)
1. [부록(Appendixes)](./support.md#41-부록appendixes)
2. [개발 환경(Development Environment)](./support.md#42-개발-환경development-environment)
3. [일정표(Schedule)](./support.md#43-일정표schedule)
5. [설계(Architecture)](./architecture.md)
1. [UML](./architecture.md#51-uml)
1. [Server Side UML](./architecture.md#511-server-side-uml)
2. [Client Side UML](./architecture.md#512-client-side-uml)
2. [의사코드(Pseudo Code)](./architecture.md#52-의사코드pseudo-code)
6. [시험(Testing)](./testing.md)
1. [유닛 테스트(Unit test)](./testing.md#61-유닛-테스트unit-test)
2. [기능 테스트(Functional Test)](./testing.md#62-기능-테스트functional-test)

View File

@ -1,16 +1,21 @@
# 1. 소개(Introduction)
> Version : 1.0.1
본 문서는 전북대학교 컴퓨터공학과의 Floor 팀에서 Scrap Yard라는 어플리케이션을 설계 및 구현하기 위한 소프트웨어 요구사항 명세서(SRS)이다.
> Version : 1.1.0
>
> Version Hash : <%= it.gitHash %>
>
> 본 문서는 전북대학교 컴퓨터공학과의 Floor 팀에서 Scrap Yard라는 어플리케이션을
> 설계 및 구현하기 위한 소프트웨어 요구사항 명세서(SRS)이자 어플리케이션의 구조를
> 나타내는 설계서이자 어플리케이션의 시험 결과 보고서이다. 정리하면 어플리케이션을
> 개발하면서 발생하는 산출물들을 정리한 문서이다.
## 1.1. 목적(Purpose)
본 문서의 목적은 프로젝트의 관련된 모든 아이디어들을 정리하고 분석해서 나열하는 것이다. 또한 프로젝트를 더 잘 이해하기 위해 이 제품이 어떻게 사용될지 예측하고 분류하고, 나중에 개발될 요소를 설명하고, 고려 중이지만 폐기될 수 있는 요구사항들을 문서화한다.
본 문서의 목적은 첫째로 프로젝트의 관련된 모든 아이디어들을 정리하고 분석해서 나열하는 것이다. 또한 프로젝트를 더 잘 이해하기 위해 이 제품이 어떻게 사용될지 예측하고 분류하고, 나중에 개발될 요소를 설명하고, 고려 중이지만 폐기될 수 있는 요구사항들을 문서화한다. 둘째로 이러한 요구사항을 해결하기 위해 만들어진 설계를 나열하고 문서화하는 것이다. 셋째로 이러한 설계로 구현된 프로그램을 시험을 하고 그 결과를 정리해서 보기좋게 문서화하고 색인하는 것이다.
## 1.2. 범위(Scope)
본 문서의 범위는 ScrapYard의 기능들과 그 환경이다.
본 문서의 범위는 ScrapYard의 기능들과 그 환경 그리고 상세 설계 및 인터페이스이다.
ScrapYard는 문서 작성 밎 문서를 아카이빙 할 수 있는 웹 어플리케이션이다. 같이 제공되는 확장기능을 통해 북마크(즐겨찾기)를 구조적으로 보관할 수 있고 미리보기를 보여줄 수 있다.
@ -27,7 +32,8 @@ ScrapYard는 문서 작성 밎 문서를 아카이빙 할 수 있는 웹 어플
## 1.4. 참고자료(References)
- [repo](https://github.com/vi117/scrap-yard)
- [repository](https://github.com/vi117/scrap-yard)
- [document repository](https://git.prelude.duckdns.org/ScrapYard/SRS)
- [react](https://reactjs.org/)
- [recoil](https://recoiljs.org/)
- [MUI](https://mui.com/)
@ -36,4 +42,7 @@ ScrapYard는 문서 작성 밎 문서를 아카이빙 할 수 있는 웹 어플
## 1.5. 개요(Overview)
2장에서는 종합적인 요구사항을 서술하고, 3장에서는 기능 및 UI에 대해서 상세한 요구사항을 설명한다.
2장과 3장은 SRS(소프트웨어 요구사항 명세서)의 양식에 맞추어 작성되었다. 2장에서는 종합적인 요구사항을 서술하고, 3장에서는 기능 및 UI에 대해서 상세한 요구사항을 설명한다.
4장은 SRS의 추가 이력사항에 대해서 서술한다. 여기에서는 어플리케이션 개발 일정표가 포함되어 있다.
5장은 어플리케이션의 상세한 설계에 대해서 서술한다.
6장은 개발된 어플리케이션의 시험과 그 결과에 대해서 서술한다.

View File

@ -1,6 +1,6 @@
# 2. 전체 시스템 개요(Overall description)
### 2.1. 제품 관점(Product perspective)
## 2.1. 제품 관점(Product perspective)
### 2.1.1. 시스템 인터페이스(System interfaces)
@ -45,17 +45,7 @@
본 프로젝트의 결과물은 다음과 같은 기능을 수행한다.
<%
const table = new Map();
it.issues.forEach((x)=>{
const category = x.title.split(":")[0];
if(!category) return;
let c = table.get(category)
if(!c){
c = [];
table.set(category,c);
}
c.push(x);
})
const table = it.table;
let index = 1;
for (const [c,issues] of table) {
%><%= `### 2.2.${index++} ${c} Operation\n\n` %><%
@ -64,7 +54,6 @@
%><%=`${subIndex++}. #${i.number} ${i.title}\n` %><%
}
%>
<%
}
%>

View File

@ -1,4 +1,4 @@
# 추가 이력 (Supporting Information)
# 요구사항 명세 추가 이력 (Requirement Specification Supporting Information)
## 4.1. 부록(Appendixes)

View File

@ -1,335 +1,130 @@
# Testing
# 6. 시험(Testing)
## 유닛 테스트
## 6.1 유닛 테스트(Unit test)
유닛 테스트로 69.6%의 Line Coverage와 73.4%의 Function Coverage를 달성했다.
다음과 같은 로그가 있다.
유닛 테스트로 63.7%의 Line Coverage와 67.5%의 Function Coverage를 달성했다.
### permission.test.ts
| name | result | duration |
| ---- | ------ | -------- |
|permission.test|✅|14ms|
|permission empty|✅|16ms|
### session.test.ts
| name | result | duration |
| ---- | ------ | -------- |
|Session : set |✅|14ms|
|Session : delete |✅|16ms|
|Login Handler : login with invalid format |✅|15ms|
|Login Handler : login with invalid password |✅|16ms|
|Login Handler : login |✅|15ms|
|Login Handler : logout with no session |✅|17ms|
|Login Handler : logout |✅|16ms|
|getSession|✅|15ms|
|getSession with invalid cookie|✅|15ms|
### user.test.ts
| name | result | duration |
| ---- | ------ | -------- |
|user.createAdminUser|✅|5ms|
### filedoc.test.ts
| name | result | duration |
| ---- | ------ | -------- |
|readDocFile|✅|12ms|
|readDocFile: not found|✅|15ms|
|readDocFile: invalid json|✅|15ms|
|saveDocFile|✅|15ms|
### methodHandle.test.ts
| name | result | duration |
| ---- | ------ | -------- |
|methodHandle: basic methods|✅|14ms|
|methodHandle: not found|✅|15ms|
|methodHandle: options|✅|17ms|
### route.test.ts
| name | result | duration |
| ---- | ------ | -------- |
|route: basic route|✅|6ms|
|route: double slash route|✅|12ms|
|route: double match|✅|16ms|
|route: test context|✅|15ms|
|route: test regex|✅|15ms|
|route: test not found|✅|2ms|
|route: encode_route|✅|12ms|
|route: router in router|✅|15ms|
### chunk.test.ts
| name | result | duration |
| ---- | ------ | -------- |
|basic chunk operation : create chunk |✅|13ms|
|basic chunk operation : delete chunk |✅|15ms|
|basic chunk operation : modify chunk |✅|16ms|
|basic chunk operation : move chunk |✅|16ms|
|basic chunk operation : invalid chunk operation |✅|16ms|
|test chunk notification operation|✅|16ms|
|test chunk conflict|✅|15ms|
|test chunk conflict resolve with history|✅|32ms|
### doc.test.ts
| name | result | duration |
| ---- | ------ | -------- |
|handleDocumentMethod|✅|5ms|
|handleTagMethod : setTag |✅|9ms|
|handleTagMethod : getTag |✅|16ms|
|handleTagMethod : conflict |✅|16ms|
### share.test.ts
| name | result | duration |
| ---- | ------ | -------- |
|handleShareGetInfo|✅|16ms|
|handleShareDocMethod|✅|2ms|
|handleShareMethod with no existing share token|✅|12ms|
### server.test.ts
| name | result | duration |
| ---- | ------ | -------- |
|server rpc test|✅|4s|
### fswatcher.test.ts
| name | result | duration |
| ---- | ------ | -------- |
|WatchFilteredReadWriter|✅|79ms|
### readWriter.test.ts
| name | result | duration |
| ---- | ------ | -------- |
|QueueReadWriter|✅|57ms|
### util.test.ts
| name | result | duration |
| ---- | ------ | -------- |
|watcher util isHidden|✅|5ms|
```
running 2 tests from ./src/auth/permission.test.ts
permission.test ... ok (8ms)
permission empty ... ok (16ms)
running 4 tests from ./src/auth/session.test.ts
Session ...
set ... ok (9ms)
delete ... ok (16ms)
ok (42ms)
Login Handler ...
login with invalid format ... ok (15ms)
login with invalid password ... ok (16ms)
login ... ok (16ms)
logout with no session ... ok (16ms)
logout ... ok (16ms)
ok (96ms)
getSession ... ok (16ms)
getSession with invalid cookie ... ok (16ms)
running 1 test from ./src/auth/user.test.ts
user.createAdminUser ... ok (15ms)
running 4 tests from ./src/document/filedoc.test.ts
readDocFile ... ok (19ms)
readDocFile: not found ... ok (16ms)
readDocFile: invalid json ... ok (16ms)
saveDocFile ... ok (15ms)
running 3 tests from ./src/router/methodHandle.test.ts
methodHandle: basic methods ... ok (8ms)
methodHandle: not found ... ok (16ms)
methodHandle: options ... ok (16ms)
running 8 tests from ./src/router/route.test.ts
route: basic route ... ok (10ms)
route: double slash route ... ok (16ms)
route: double match ... ok (16ms)
route: test context ... ok (16ms)
route: test regex ... ok (16ms)
route: test not found ... ok (16ms)
route: encode_route ... ok (2ms)
route: router in router ... ok (13ms)
running 4 tests from ./src/rpc/chunk.test.ts
basic chunk operation ...
create chunk ... ok (19ms)
delete chunk ... ok (15ms)
modify chunk ... ok (15ms)
move chunk ... ok (15ms)
invalid chunk operation ... ok (17ms)
ok (98ms)
test chunk notification operation ... ok (15ms)
test chunk conflict ... ok (16ms)
test chunk conflict resolve with history ... ok (32ms)
running 2 tests from ./src/rpc/doc.test.ts
handleDocumentMethod ... ok (4ms)
handleTagMethod ...
setTag ... ok (13ms)
getTag ... ok (15ms)
conflict ... ok (15ms)
ok (61ms)
running 3 tests from ./src/rpc/share.test.ts
handleShareGetInfo ... ok (18ms)
handleShareDocMethod ... ok (15ms)
handleShareMethod with no existing share token ... ok (16ms)
running 1 test from ./src/server.test.ts
server rpc test ... ok (1s)
running 3 tests from ./src/setting.test.ts
setting: basic ... ok (35ms)
setting: default value ... ok (7ms)
setting: defered register ... ok (16ms)
test result: ok. 35 passed (15 steps); 0 failed; 0 ignored; 0 measured; 0 filtered out (2s)
```
### Total
| name | passed | Steps | Failed | duration |
| ---- | ------ | ----- | ------ | -------- |
| ok | 35 | 15 | 0 | 4726ms |
## 기능 테스트
## 6.2 기능 테스트(Functional Test)
### Chunk
<% const keys = Array.from(it.table.keys()); %>
<table>
<thead>
<tr>
<th>ID</th>
<th>Content</th>
<th>Procedure</th>
<th>Test Data</th>
<th>P/F</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>Focus/Unfocus</td>
<td>1. 청크를 클릭한다.</td>
<td></td>
<td>P</td>
</tr>
<tr>
<td>2</td>
<td>remove</td>
<td>1. 청크를 삭제하는 버튼을 클릭한다.</td>
<td></td>
<td>P</td>
</tr>
<tr>
<td>3-1</td>
<td>render - markdown</td>
<td>1. 마크다운 청크 렌더링을 확인한다.</td>
<td> # 제목 </td>
<td>P</td>
</tr>
<tr>
<td>3-2</td>
<td>render - latex</td>
<td>1. LaTex 청크 렌더링을 확인한다.</td>
<td> sum^n_{n=0}n = \frac{n(n+1)}2$$ </td>
<td>P</td>
</tr>
<tr>
<td>3-3</td>
<td>render - link</td>
<td>1. Image 청크 렌더링을 확인한다.</td>
<td>http://picsum.photos</td>
<td>P</td>
</tr>
<tr>
<td>4</td>
<td>previews</td>
<td>1. Katex 청크의 미리보기를 본다.</td>
<td> sum^n_{n=0}n = \frac{n(n+1)}2$$ </td>
<td>F</td>
</tr>
<tr>
<td>10</td>
<td>autocomplete</td>
<td>1. <kbd>Ctrl+Space</kbd>를 눌러 자동완성을 시도한다.</td>
<td></td>
<td>F</td>
</tr>
<tr>
<td>11</td>
<td>swap positions</td>
<td>1. 청크의 위치를 바꾼다.</td>
<td></td>
<td>P</td>
</tr>
<tr>
<td>27-1</td>
<td>edit</td>
<td>1. 청크를 수정한다.</td>
<td></td>
<td>P</td>
</tr>
<tr>
<td>27-2</td>
<td>edit chunk conflict</td>
<td>1. 청크를 수정모드에 들어간다.</td>
<td></td>
<td>F</td>
</tr>
</tbody>
</table>
### Document
<table>
<thead>
<tr>
<th>ID</th>
<th>Content</th>
<th>Procedure</th>
<th>Test Data</th>
<th>P/F</th>
</tr>
</thead>
<tbody>
<tr>
<td>5</td>
<td>view Chunk</td>
<td>1. 문서를 열어 청크가 렌더링되는지 본다.</td>
<td>test.syd</td>
<td>P</td>
</tr>
<tr>
<td>7</td>
<td>add/delete tag</td>
<td>1. 문서에 태그를 추가한다.<br>2. 문서에 태그를 삭제한다.</td>
<td>A</td>
<td>P</td>
</tr>
<tr>
<td>8</td>
<td>Drag And Drop Upload,</td>
<td>1. 텍스트를 드래그한다.</td>
<td></td>
<td>P</td>
</tr>
</tbody>
</table>
### File
<table>
<thead>
<tr>
<th>ID</th>
<th>Content</th>
<th>Procedure</th>
<th>Test Data</th>
<th>P/F</th>
</tr>
</thead>
<tbody>
<tr>
<td>14</td>
<td>create/delete/rename file</td>
<td>1. 파일을 만든다.</td>
<td>test.txt</td>
<td>P</td>
</tr>
<tr>
<td>15</td>
<td>upload/download files</td>
<td>1. 파일을 업로드한다.</td>
<td>test.txt</td>
<td>P</td>
</tr>
<tr>
<td>18</td>
<td>export document</td>
<td>1. export 버튼을 누른다.</td>
<td></td>
<td>F</td>
</tr>
</tbody>
</table>
### Search
<table>
<thead>
<tr>
<th>ID</th>
<th>Content</th>
<th>Procedure</th>
<th>Test Data</th>
<th>P/F</th>
</tr>
</thead>
<tbody>
<tr>
<td>16</td>
<td>Document Search</td>
<td>1. 검색버튼을 눌러 검색을 한다.</td>
<td>chunk</td>
<td>F</td>
</tr>
</tbody>
</table>
### Stash
<table>
<thead>
<tr>
<th>ID</th>
<th>Content</th>
<th>Procedure</th>
<th>Test Data</th>
<th>P/F</th>
</tr>
</thead>
<tbody>
<tr>
<td>17</td>
<td>render</td>
<td>1. 스태시가 그려지는지 확인한다</td>
<td></td>
<td>P</td>
</tr>
<tr>
<td>19</td>
<td>add</td>
<td>1. 청크를 추가한다</td>
<td></td>
<td>P</td>
</tr>
<tr>
<td>20</td>
<td>remove</td>
<td>1. 청크를 삭제한다</td>
<td></td>
<td>P</td>
</tr>
<tr>
<td>21</td>
<td>Drag and Drop to Document</td>
<td>1. 청크로부터 문서로 청크를 옮긴다.</td>
<td></td>
<td>P</td>
</tr>
</tbody>
</table>
### Management
<table>
<thead>
<tr>
<th>ID</th>
<th>Content</th>
<th>Procedure</th>
<th>Test Data</th>
<th>P/F</th>
</tr>
</thead>
<tbody>
<tr>
<td>22</td>
<td>Login</td>
<td>1. 비밀번호를 입력한다.</td>
<td>admin</td>
<td>F</td>
</tr>
<tr>
<td>24</td>
<td>Localization</td>
<td>1. 다른언어를 지원하는지 언어를 바꿔 확인한다</td>
<td></td>
<td>F</td>
</tr>
</tbody>
</table>
<% keys.forEach(c=>{
const issues = it.table.get(c);
const ts = issues.map(x=> it.testcases.filter(y=>y.id==x.number)).flat();
if(ts?.length == 0) return; %>
<%~`\n\n### ${c}\n\n`%>
<%~"<table>"%>
<%~"<thead>"%>
<%~"<tr>"%>
<%~"<th>ID</th>"%>
<%~"<th>Content</th>"%>
<%~"<th>Procedure</th>"%>
<%~"<th>Test Data</th>"%>
<%~"<th>P/F</th>"%>
<%~"</tr>"%>
<%~"</thead>"%>
<%~"<tbody>"%>
<% ts.forEach((y,i)=>{
const id = y.subId ? `${y.id}-${y.subId}` : y.id;%>
<%~"<tr>"%>
<%~`<td>${id}</td>`%>
<%~`<td>${y.content}</td>`%>
<%~`<td>${y.procedure}</td>`%>
<%~`<td>${y.testData ?? ""}</td>`%>
<%~`<td>${y.pass ? "✅" : "❌"}</td>`%>
<%~"</tr>"%>
<%});%>
<%~"</tbody>"%>
<%~"</table>"%>
<% });%>

View File

@ -14,9 +14,14 @@ import "https://deno.land/std@0.136.0/dotenv/load.ts";
* console.log(issues);
* ```
*/
export async function getIssues(repo: string, token: string): Promise<Issue[]> {
export async function getIssues(repo: string, token?: string): Promise<Issue[]> {
//check https://docs.github.com/en/rest/reference/issues#list-repository-issues
const res = await fetch(`https://api.github.com/repos/${repo}/issues?per_page=100&labels=feature&state=all`, {
const url = `https://api.github.com/repos/${repo}/issues?per_page=100`;
if(!token) {
const res = await fetch(url);
return await res.json();
}
const res = await fetch(url, {
headers: {
Accept: 'application/vnd.github.v3+json',
Authorization: `Token ${token}`
@ -28,15 +33,7 @@ export async function getIssues(repo: string, token: string): Promise<Issue[]> {
if (import.meta.main) {
const args = parse(Deno.args);
const token = args.token ?? Deno.env.get("GITHUB_TOKEN");
if(typeof token !== "string"){
console.error("invalid type: token must be string");
Deno.exit(1);
}
if(!token) {
console.error("GITHUB_TOKEN is not set");
Deno.exit(1);
}
let token = args.token ?? Deno.env.get("GITHUB_TOKEN");
const issues = await getIssues("vi117/scrap-yard", token);
issues.sort((a, b) => a.number - b.number);
const content = JSON.stringify(issues, null, 2);

View File

@ -4,6 +4,20 @@ import { WriterHandler } from "https://deno.land/std@0.143.0/log/handlers.ts";
import * as Eta from "https://deno.land/x/eta@v1.12.3/mod.ts";
import { Issue } from "./githubType.ts";
import testcaseData from "./testcases.json" assert { type: "json"};
type Testcase = {
id: number,
subId: number | null,
content: string,
procedure: string,
testData: string| null,
expected: string,
actual: string,
pass: boolean
}
const testcases = testcaseData as Testcase[];
class StderrHandler extends WriterHandler {
protected _writer: Deno.Writer;
#encoder: TextEncoder;
@ -19,18 +33,20 @@ class StderrHandler extends WriterHandler {
}
interface Book {
sections: Section[];
sections: BookItem[];
}
interface Section {
//chapter or separtor or PartTitle
type Separator = "Separator";
//type PartTitle = ;
type BookItem = Separator | {
Chapter: Chapter;
}
interface Chapter {
name: string;
content: string;
/** section number */
number?: number[];
sub_items: Section[];
sub_items: BookItem[];
path?: string;
source_path?: string;
parent_names: string[];
@ -62,6 +78,17 @@ async function getIssues(){
return issues;
}
function toHeadId(name: string){
return name.replaceAll(/[^A-Za-z\s0-9]/gi,"").toLocaleLowerCase().replaceAll(" ","-");
}
async function getCurrentGitHash() {
const res = await Deno.run({cmd:["git", "rev-parse", "HEAD"], stdout:"piped"});
const hash = new TextDecoder().decode(await res.output()).trim();
res.close();
return hash;;
}
async function main(args: string[]) {
if (args.length > 1) {
//log.info(`args: ${JSON.stringify(args)}`);
@ -70,16 +97,37 @@ async function main(args: string[]) {
}
}
const issues = await getIssues();
const table = new Map();
issues.forEach((x)=>{
const category = x.title.split(":")[0];
if(!category) return;
let c = table.get(category)
if(!c){
c = [];
table.set(category,c);
}
c.push(x);
});
const gitHash = await getCurrentGitHash();
log.info(`start`);
log.info(`start\n`);
const data = await readAll(Deno.stdin);
const jsonText = new TextDecoder().decode(data);
await Deno.writeTextFile("log.json", jsonText);
const [context, book] = JSON.parse(jsonText) as [any, Book];
book.sections.forEach(x=>{
if(x === "Separator"){
//skip
}
else {
x.Chapter.content = Eta.render(x.Chapter.content, {
issues: issues
issues: issues,
table: table,
gitHash: gitHash,
toHeadId: toHeadId,
testcases: testcases
}) as string;
}
})
//Deno.stderr.writeSync(new TextEncoder().encode(`context: ${JSON.stringify(context)}\n`));
console.log(JSON.stringify(book));

View File

@ -1,3 +1,5 @@
import testcaseData from "./testcases.json" assert { type: "json"};
type Testcase = {
id: number,
subId: number | null,
@ -9,261 +11,11 @@ type Testcase = {
pass: boolean
}
const testcase: Testcase[] = [
{
"id": 1,
"subId": null,
"content": "Focus/Unfocus",
"procedure": "1. 청크를 클릭한다.",
"testData": null,
"expected": "",
"actual": "",
"pass": true
},
{
"id": 2,
"subId": null,
"content": "remove",
"procedure": "1. 청크를 삭제하는 버튼을 클릭한다.",
"testData": null,
"expected": "",
"actual": "",
"pass": true
},
{
"id": 3,
"subId": 1,
"content": "render - markdown",
"procedure": "1. 마크다운 청크 렌더링을 확인한다.",
"testData": " # 제목 ",
"expected": "",
"actual": "",
"pass": true
},
{
"id": 3,
"subId": 2,
"content": "render - latex",
"procedure": "1. LaTex 청크 렌더링을 확인한다.",
"testData": " sum^n_{n=0}n = \\frac{n(n+1)}2$$ ",
"expected": "",
"actual": "",
"pass": true
},
{
"id": 3,
"subId": 3,
"content": "render - link",
"procedure": "1. Image 청크 렌더링을 확인한다.",
"testData": " http://picsum.photos/200/300 ",
"expected": "",
"actual": "",
"pass": true
},
{
"id": 4,
"subId": null,
"content": "previews",
"procedure": "1. Katex 청크의 미리보기를 본다.",
"testData": " sum^n_{n=0}n = \\frac{n(n+1)}2$$ ",
"expected": "",
"actual": "",
"pass": false
},
{
"id": 10,
"subId": null,
"content": "autocomplete",
"procedure": "1. 자동완성을 시험한다.",
"testData": null,
"expected": "",
"actual": "",
"pass": false
},
{
"id": 11,
"subId": null,
"content": "swap positions",
"procedure": "1. 청크의 위치를 바꾼다.",
"testData": null,
"expected": "",
"actual": "",
"pass": true
},
{
"id": 27,
"subId": 1,
"content": "edit",
"procedure": "1. 청크를 수정한다.",
"testData": null,
"expected": "",
"actual": "",
"pass": true
},
{
"id": 27,
"subId": 2,
"content": "edit chunk conflict",
"procedure": "1. 청크를 수정모드에 들어간다.",
"testData": null,
"expected": "",
"actual": "",
"pass": false
},
{
"id": 5,
"subId": null,
"content": "view Chunk",
"procedure": "1. 문서를 열어 청크가 렌더링되는지 본다.",
"testData": "test.syd",
"expected": "",
"actual": "",
"pass": true
},
{
"id": 7,
"subId": null,
"content": "add/delete tag",
"procedure": "1. 문서에 태그를 추가한다.<br>2. 문서에 태그를 삭제한다.",
"testData": "A",
"expected": "",
"actual": "",
"pass": true
},
{
"id": 8,
"subId": null,
"content": "Drag And Drop Upload,",
"procedure": "1. 텍스트를 드래그한다.",
"testData": null,
"expected": "",
"actual": "",
"pass": true
},
{
"id": 14,
"subId": null,
"content": "create/delete/rename file",
"procedure": "1. 파일을 만든다.",
"testData": "test.txt",
"expected": "",
"actual": "",
"pass": true
},
{
"id": 15,
"subId": null,
"content": "upload/download files",
"procedure": "1. 파일을 업로드한다.",
"testData": "test.txt",
"expected": "",
"actual": "",
"pass": true
},
{
"id": 18,
"subId": null,
"content": "export document",
"procedure": "1. export 버튼을 누른다.",
"testData": null,
"expected": "",
"actual": "",
"pass": false
},
{
"id": 17,
"subId": null,
"content": "render",
"procedure": "1. 스태시가 그려지는지 확인한다",
"testData": null,
"expected": "",
"actual": "",
"pass": true
},
{
"id": 19,
"subId": null,
"content": "add",
"procedure": "1. 청크를 추가한다",
"testData": null,
"expected": "",
"actual": "",
"pass": true
},
{
"id": 20,
"subId": null,
"content": "remove",
"procedure": "1. 청크를 삭제한다",
"testData": null,
"expected": "",
"actual": "",
"pass": true
},
{
"id": 21,
"subId": null,
"content": "Drag and Drop to Document",
"procedure": "1. 청크로부터 문서로 청크를 옮긴다.",
"testData": null,
"expected": "",
"actual": "",
"pass": true
},
{
"id": 22,
"subId": null,
"content": "Login",
"procedure": "1. 비밀번호를 입력한다.",
"testData": "admin",
"expected": "",
"actual": "",
"pass": false
},
{
"id": 24,
"subId": null,
"content": "Localization",
"procedure": "1. 다른언어를 지원하는지 언어를 바꿔 확인한다",
"testData": null,
"expected": "",
"actual": "",
"pass": false
},
{
"id": 16,
"subId": null,
"content": "Document Search",
"procedure": "1. 검색해본다.",
"testData": null,
"expected": "",
"actual": "",
"pass": false
},
{
"id": 85,
"subId": null,
"content": "Permission",
"procedure": "1. 각각의 기능들에 권한없이 시도한다",
"testData": null,
"expected": "",
"actual": "",
"pass": true
}
]
const testcase: Testcase[] = testcaseData;
import {Issue} from "./githubType.ts"
async function readContent(path?: string): Promise<string> {
let content = "[]";
if (path) {
content = await Deno.readTextFile(path);
}
else throw new Error("No input provided. path or stdin.");
return content;
}
const data = await readContent("../build/issues.json")
const data = await Deno.readTextFile("../cache/issues.json")
const issues = JSON.parse(data) as Issue[]
const table = new Map<string, Issue[]>();
@ -276,7 +28,7 @@ const table = new Map<string, Issue[]>();
table.set(category,c);
}
c.push(x);
})
});
const keys = Array.from(table.keys());

252
tools/testcases.json Normal file
View File

@ -0,0 +1,252 @@
[
{
"id": 1,
"subId": null,
"content": "Focus/Unfocus",
"procedure": "1. 청크를 클릭한다.",
"testData": null,
"expected": "",
"actual": "",
"pass": true
},
{
"id": 2,
"subId": null,
"content": "remove",
"procedure": "1. 청크를 삭제하는 버튼을 클릭한다.",
"testData": null,
"expected": "",
"actual": "",
"pass": true
},
{
"id": 3,
"subId": 1,
"content": "render - markdown",
"procedure": "1. 마크다운 청크 렌더링을 확인한다.",
"testData": " # 제목 ",
"expected": "",
"actual": "",
"pass": true
},
{
"id": 3,
"subId": 2,
"content": "render - latex",
"procedure": "1. LaTex 청크 렌더링을 확인한다.",
"testData": " sum^n_{n=0}n = \\frac{n(n+1)}2$$ ",
"expected": "",
"actual": "",
"pass": true
},
{
"id": 3,
"subId": 3,
"content": "render - link",
"procedure": "1. Image 청크 렌더링을 확인한다.",
"testData": " http://picsum.photos/200/300 ",
"expected": "",
"actual": "",
"pass": true
},
{
"id": 4,
"subId": null,
"content": "previews",
"procedure": "1. Katex 청크의 미리보기를 본다.",
"testData": " sum^n_{n=0}n = \\frac{n(n+1)}2$$ ",
"expected": "",
"actual": "",
"pass": true
},
{
"id": 10,
"subId": null,
"content": "autocomplete",
"procedure": "1. 자동완성을 시험한다.",
"testData": null,
"expected": "",
"actual": "",
"pass": false
},
{
"id": 11,
"subId": null,
"content": "swap positions",
"procedure": "1. 청크의 위치를 바꾼다.",
"testData": null,
"expected": "",
"actual": "",
"pass": true
},
{
"id": 27,
"subId": 1,
"content": "edit",
"procedure": "1. 청크를 수정한다.",
"testData": null,
"expected": "",
"actual": "",
"pass": true
},
{
"id": 27,
"subId": 2,
"content": "edit chunk conflict",
"procedure": "1. 청크를 수정모드에 들어간다.",
"testData": null,
"expected": "",
"actual": "",
"pass": false
},
{
"id": 5,
"subId": null,
"content": "view Chunk",
"procedure": "1. 문서를 열어 청크가 렌더링되는지 본다.",
"testData": "test.syd",
"expected": "",
"actual": "",
"pass": true
},
{
"id": 7,
"subId": null,
"content": "add/delete tag",
"procedure": "1. 문서에 태그를 추가한다.<br>2. 문서에 태그를 삭제한다.",
"testData": "A",
"expected": "",
"actual": "",
"pass": true
},
{
"id": 8,
"subId": null,
"content": "Drag And Drop Upload,",
"procedure": "1. 텍스트를 드래그한다.",
"testData": null,
"expected": "",
"actual": "",
"pass": true
},
{
"id": 14,
"subId": null,
"content": "create/delete/rename file",
"procedure": "1. 파일을 만든다.",
"testData": "test.txt",
"expected": "",
"actual": "",
"pass": true
},
{
"id": 15,
"subId": null,
"content": "upload/download files",
"procedure": "1. 파일을 업로드한다.",
"testData": "test.txt",
"expected": "",
"actual": "",
"pass": true
},
{
"id": 18,
"subId": null,
"content": "export document",
"procedure": "1. export 버튼을 누른다.",
"testData": null,
"expected": "",
"actual": "",
"pass": false
},
{
"id": 17,
"subId": null,
"content": "render",
"procedure": "1. 스태시가 그려지는지 확인한다",
"testData": null,
"expected": "",
"actual": "",
"pass": true
},
{
"id": 19,
"subId": null,
"content": "add",
"procedure": "1. 청크를 추가한다",
"testData": null,
"expected": "",
"actual": "",
"pass": true
},
{
"id": 20,
"subId": null,
"content": "remove",
"procedure": "1. 청크를 삭제한다",
"testData": null,
"expected": "",
"actual": "",
"pass": true
},
{
"id": 21,
"subId": null,
"content": "Drag and Drop to Document",
"procedure": "1. 청크로부터 문서로 청크를 옮긴다.",
"testData": null,
"expected": "",
"actual": "",
"pass": true
},
{
"id": 22,
"subId": null,
"content": "Login",
"procedure": "1. 비밀번호를 입력한다.",
"testData": "admin",
"expected": "",
"actual": "",
"pass": true
},
{
"id": 24,
"subId": null,
"content": "Localization",
"procedure": "1. 다른언어를 지원하는지 언어를 바꿔 확인한다",
"testData": null,
"expected": "",
"actual": "",
"pass": false
},
{
"id": 25,
"subId": null,
"content": "Theme",
"procedure": "1. Theme를 바꾸어서 제대로 변경되는지 시험해본다.",
"testData": null,
"expected": "",
"actual": "",
"pass": true
},
{
"id": 16,
"subId": null,
"content": "Document Search",
"procedure": "1. 검색해본다.",
"testData": null,
"expected": "",
"actual": "",
"pass": false
},
{
"id": 85,
"subId": null,
"content": "Permission",
"procedure": "1. 각각의 기능들에 권한없이 시도한다",
"testData": null,
"expected": "",
"actual": "",
"pass": true
}
]

206
tools/unittestToDoc.ts Normal file
View File

@ -0,0 +1,206 @@
const test_stdout: string = `running 2 tests from ./src/auth/permission.test.ts
permission.test ... ok (14ms)
permission empty ... ok (16ms)
running 4 tests from ./src/auth/session.test.ts
Session ...
set ... ok (14ms)
delete ... ok (16ms)
ok (49ms)
Login Handler ...
login with invalid format ... ok (15ms)
login with invalid password ... ok (16ms)
login ... ok (15ms)
logout with no session ... ok (17ms)
logout ... ok (16ms)
ok (94ms)
getSession ... ok (15ms)
getSession with invalid cookie ... ok (15ms)
running 1 test from ./src/auth/user.test.ts
user.createAdminUser ... ok (5ms)
running 4 tests from ./src/document/filedoc.test.ts
readDocFile ... ok (12ms)
readDocFile: not found ... ok (15ms)
readDocFile: invalid json ... ok (15ms)
saveDocFile ... ok (15ms)
running 3 tests from ./src/router/methodHandle.test.ts
methodHandle: basic methods ... ok (14ms)
methodHandle: not found ... ok (15ms)
methodHandle: options ... ok (17ms)
running 8 tests from ./src/router/route.test.ts
route: basic route ... ok (6ms)
route: double slash route ... ok (12ms)
route: double match ... ok (16ms)
route: test context ... ok (15ms)
route: test regex ... ok (15ms)
route: test not found ... ok (2ms)
route: encode_route ... ok (12ms)
route: router in router ... ok (15ms)
running 4 tests from ./src/rpc/chunk.test.ts
basic chunk operation ...
create chunk ... ok (13ms)
delete chunk ... ok (15ms)
modify chunk ... ok (16ms)
move chunk ... ok (16ms)
invalid chunk operation ... ok (16ms)
ok (96ms)
test chunk notification operation ... ok (16ms)
test chunk conflict ... ok (15ms)
test chunk conflict resolve with history ... ok (32ms)
running 2 tests from ./src/rpc/doc.test.ts
handleDocumentMethod ... ok (5ms)
handleTagMethod ...
setTag ... ok (9ms)
getTag ... ok (16ms)
conflict ... ok (16ms)
ok (58ms)
running 3 tests from ./src/rpc/share.test.ts
handleShareGetInfo ... ok (16ms)
handleShareDocMethod ... ok (2ms)
handleShareMethod with no existing share token ... ok (12ms)
running 1 test from ./src/server.test.ts
server rpc test ... ok (4s)
running 1 test from ./src/watcher/fswatcher.test.ts
WatchFilteredReadWriter ... ok (79ms)
running 1 test from ./src/watcher/readWriter.test.ts
QueueReadWriter ... ok (57ms)
running 1 test from ./src/watcher/util.test.ts
watcher util isHidden ... ok (5ms)
test result: ok. 35 passed (15 steps); 0 failed; 0 ignored; 0 measured; 0 filtered out (6s)
`;
interface TestStep{
name: string;
result: boolean;
time: number;
}
interface TestResult{
name: string;
result: boolean;
time: number;
steps: TestStep[];
}
interface TestFiles{
path: string;
tests: TestResult[];
}
const lines = test_stdout.split("\n");
let cur = 0;
function getMs(time: string){
if (time.endsWith("ms")){
return parseInt(time.replace("ms", ""));
}
return parseInt(time.replace("s", "")) * 1000;
}
let testFiles: TestFiles[] = [];
function parseTest(): TestResult {
const m = /^(.*) \.\.\.(.*)/.exec(lines[cur]);
let steps: TestStep[] = []
if(m){
const name = m[1];
const status = m[2];
let result = status.includes("ok");
let time = 0;
if(status === ""){
cur++;
while(!lines[cur].startsWith("ok")){
const s = /^ (.*) \.\.\. (.*) \((\d+m?s)\)/.exec(lines[cur]);
if(!s){
console.log("unexpected line: " + lines[cur]);
Deno.exit(1);
}
const stepname = s[1];
const stepstatus = s[2];
const steptime = s[3];
//console.log(`${name} ${stepname} ${stepstatus} ${steptime}`);
steps.push({name: stepname, result: stepstatus.includes("ok"), time: getMs(steptime)});
cur++;
}
result = lines[cur].startsWith("ok");
time = getMs(lines[cur].match(/\d+m?s/)![0]);
}
else{
time = getMs(status.match(/\d+m?s/)![0]);
}
//console.log(name, result, time);
cur++;
return {name, result, time, steps};
}
console.log("unexpected line: " + lines[cur]);
Deno.exit(1);
}
while(cur < lines.length){
const line = lines[cur];
if(line === ""){
cur++;
break;
}
const m = /^running (\d+) tests? from (.+)/.exec(lines[cur]);
if(m){
const num = parseInt(m[1]);
const file = m[2];
//console.log(`${num} tests from ${file}`);
cur++;
let tests = [];
for(let i = 0; i < num; i++){
const test = parseTest();
tests.push(test);
}
testFiles.push({path: file, tests});
}
else {
console.log("%c unexpected : "+line,"color: red");
break;
}
//console.log("endwith " + cur);
}
const lastResultRegex = /test result: (.*)\. (\d+) passed \((\d+) steps\); (\d+) failed; (\d+) ignored; (\d+) measured; (\d+) filtered out \((\d+m?s)\)/;
const mm = lastResultRegex.exec(lines[cur]);
if(!mm){
console.log("unexpected line: " + lines[cur]);
Deno.exit(1);
}
const mok = mm[1];
const numPassed = parseInt(mm[2]);
const numSteps = parseInt(mm[3]);
const numFailed = parseInt(mm[4]);
const numIgnored = parseInt(mm[5]);
const numMeasured = parseInt(mm[6]);
const numFiltered = parseInt(mm[7]);
const time = getMs(mm[8]);
import * as path from "https://deno.land/std@0.142.0/path/mod.ts";
let duration = 0;
//console.log(testFiles);
testFiles.forEach(file => {
console.log(`### ${path.basename(file.path)}`);
console.log(`| name | result | duration |`);
console.log(`| ---- | ------ | -------- |`);
file.tests.forEach(test => {
if(test.steps.length > 0){
test.steps.forEach(step => {
console.log(`|${test.name + " : " + step.name} |${step.result ? "✅" : "❌"}|${step.time}ms|`);
duration += step.time;
});
}
else{
console.log(`|${test.name}|${test.result ? "✅" : "❌"}|${test.time}ms|`);
duration += test.time;
}
});
});
console.log(`### Total`);
console.log(`| name | passed | Steps | Failed | duration |`);
console.log(`| ---- | ------ | ----- | ------ | -------- |`);
console.log(`| ${mok} | ${numPassed} | ${numSteps} | ${numFailed} | ${duration}ms |`);