안녕하세요 깍돌이입니다.

 

웹 자동화를 하기 위해서 사용되어지는 waitForNavigationAPI 랑 isIntersectingViewport API 에 대해서 간단 포스팅을 진행하려고 합니다.

 

상세 포스팅은 자동화 툴에서 따로 진행 하려고합니다. ( 공식 홈페이지 참조 및 관련 샘플 까지 ) 

이번 설명은 제목과 같이 Naver Cloud Platform 에 있는 웹을 샘플로 설명 드리겠습니다.

 

우선 간단하게 MPA와 SPA에 대해서 알고 가야 합니다.

 

아래 velog.io 에 있는 간단 설명 출처 입니다.

https://velog.io/@thms200/SPA-vs.-MPA

 

SPA vs. MPA

SPA, MPA란 무엇일까? 차분히 정리해보자. 1. 정의 글자 그대로 단순(?)하게 해석하면, SPA(Single Page Application)는 한 개(single)의 Page로 구성된 application 이고, MPA(Multi Page application)

velog.io

 

간단하게 핵심 요약 을 하자면 아래의 그림입니다.   ( 조금 수정해서 말씀드리고 싶지만 )

다른 부분은 제외하고 Page Reload 가 되는 시점을 봐야합니다.

SPA , MPA 설명함에 있어 많이 생략될수있는 점 양해 부탁드립니다.

MPA에서 보시면 유의미하게 봐야 할 부분이 렌더링 및 페인트 ( 화면 그리기 ) 는 브라우저에서 발생합니다.   하지만

MPA는 Element ( 브라우저의 syntax tree 중 HTML Element ) 를 미리 서버에서 만들어 놓은 후 클라이언트에게 던져주는 것익오 SPA의 경우 Element를 서버에서 그려주는것이 아닌 클라이언트(브라우저) 에서 JS를 실행하여 만들고 그다음에 브라우저가 렌더링을 하는 방식입니다. ( syntax tree에서 렌더링을 하는 부분은 매우 복잡하고 내용이 길어지기 때문에 생략 합니다. ) 

 

요약을 하자면 MPA는 밑그림을 백엔드에서 그려서 던져주고 SPA는 밑그림 부터 내가 그려야 합니다.    

(그렇기 떄문에 Performance 탭에서 확인시 JS의 사용시간이 SPA가 확연이 긴것이 확인됩니다. ) 

 

MPA = 다음카페

 

로드 후 기본적인 스크립팅 후 렌더 및 페인트가 이루어집니다.

 

SPA = React 공식 홈페이지 의 블로그 커뮤니티 탭 ( 홈은 Code 를 보여주는 부분이 좀더 무거움 )

 

두개의 차이를 느끼셨나요?

 

다음카페의 경우 Dotax라는 카페의 게시판이기 때문에 글에 대한 데이터 및 Pagination 등 꽤 복잡하게 이루어져있는 페이지에 비해 리액트 공식홈페이지의 블로그 커뮤니티 탭의 렌더링은 JS 의 위주로 준비 및 세팅이 되고있습니다. 

 

여기서 이제 유의 해야 할 점이 있습니다.

waitForNavigation API

우선 공식 puppeteer github에 있는 waitForNavigation 의 자료 설명입니다.  아래의 사용 예시까지 잘되어있습니다.

 

navigation 이 완료된 후  -> 이부분이 핵심인데요 navigation 이 완료되는 조건이 중요합니다.

해당 API 의 프로퍼티들을 보면 load, documentloaded ( dcl 이라고도합니다. )  이부분들이 의미하는 바는 브라우저의 Document ( 문서 ) 의 로드가 어디까지 되었느냐에 따라 체크를 할수있도록 되어있습니다. 

한창 JS , Browser , HTML , CSS등 초반에 공부하면 무조건 나오는 내용 중 하나인데요 좀더 깊어지면 

FP ( First Paint )
FCP ( First Contentful Paint ) 
DCL (DocumentContentLoad)
L (Load ) 
LCP (Largest Contentful Paint )
FMP ( First Meaningful Paint ) 

등 다 튀어나올수 있습니다 . 포스팅은 최대한 짧게 하려고 하니 내용은 생략하지만  핵심만 딱 설명이 쉽지 않네요 ㅎ;;

 

여튼! 위의 옵션을 보았을때 L, DCL , networkidle 0 같은걸로 보아 브라우저의 network 이벤트 타임  혹은 문서 로딩 을 기준으로 체크 하는 점을 볼수 있습니다.   

 

그럼 여기서 중요한 점이 뭐냐면  해당 옵션 및 이벤트 타임은 새롭게 그려질때 만 발생한다는 점입니다.

네이버 클라우드 플랫폼을 기준으로 하고 있으니 예시를 들어 드리자면

로그인창에서 -> 콘솔창으로 넘어갈때는 사용하여도 오류가 나지 않습니다. 콘솔창으로 넘어가기 때문 (새롭게 렌더링 )

하지만 서버를 생성 하는 부분

 

 

여기 페이지인 server/create 페이지부터는 서버 생성을 하기 위해 이미지를 선택하고 다음> 버튼을 눌러도 위에 navigation API 를 사용시 무조건 timeout이 발생합니다.     

이유를 다시 말씀드리면 위의 서버 페이지의 주소는 아래와 같습니다.

https://console.ncloud.com/vpc-compute/server/create

그리고 이미지를 선택하여 다음을 선택시 나오는 화면 및 링크는 아래와 같습니다.

링크 : https://console.ncloud.com/vpc-compute/server/create

화면 :

그렇습니다. 화면은 변경되었지만 ?  URL은 변경되지않았습니다.  리액트 홈페이지의 경우 React의 Router를 사용하여서 화면을 렌더링 해주기때문에  URL 이 변화가 생기면서 렌더링을 하고있지만 해당 홈페이지도 아마 확인시 새로운 networkidle을 받아 오지는 않을것으로 보입니다. ( 하지만 SSR을 적용하였다면 받아 올수 있습니다. ) 

그렇기 때문에 새로운 DCL 이나 L, FMP  ( 아주 대중적으로 jquery 쓰시는 분들은 jquery.ready 도 있습니다. ) 등이 새롭게 이벤트가 캐치 되지 않기 때문에 waitForNavigation 는 매우 높은 확률로 timeout이 나고 이부분을 모르는 puppeteer 사용자는 의문을 표하게 되며 무조건 timeout을 스택오버플로우에 검색하게 될수 있습니다.

 

그럼 UI 자동화 툴을 이용하여서 페이지를 넘기거나 다른 작업이 시작전 정상적으로 로드 됨을 체크하기위해서 해당 API 를 썻던 경우 위의 화면과 같은 CSR의 경우 어떻게 확인을 하는것이 좋을까 에 대한 값은 아래의 API 가 있습니다.

 

 

isIntersectingViewport API

공식 홈페이지부터 보고 가시겠습니다~!

returns : 요소가 뷰 포트에 표시되면 true를 반환합니다. 

 

여기서 뷰 포트는 현재 브라우저의 뷰 라고 할수 있습니다.

 

그럼 이걸로 페이지 가 정상 렌더 됐는지 어떻게 확인할까에 대해서 위의 서버 생성 페이지를 한번 보겠습니다.

 

상단 UL 부분

 

하단 페이지 렌더 부분

 

 

여기서 특이점을 찾으셨나요?

 

4개의 DIV가 있고 현재 보여주는 1개 제외하곤 display :none을 통해서 처리하고있습니다.

 

? 위에 5개가있던데 1개는 어디있을까 하고 확인해보니

 

마지막 생성시에 붙는점이 확인되었습니다. (의도는 잘 모르겠습니다.)

 

그럼 이제 이 페이지에서 내가 서버를 선택하고 -> 서버 생성 페이지로 정상적으로 들어왔는지를 체크해야합니다.

 

일단 2가지가 있는데 isIntersectingViewport 를 먼저 실행하진않습니다.  이유가 뭐냐면 저기 위에있는 html , div, ul 등을 HTML 태그라고 하면서 Element라고 표현하기도합니다. 그리고 이 요소들이 붙는 부분이 DOM 인데 이 Element들이 DOM 에 붙지 않았다면 당연히 브라우저는 렌더링을 해주지 않기 때문에 

 

우선적으로 DOM 에 Element가 붙었는지를 체크합니다. 체크를 하는 Element를 선택하는 기준은 모두가 다르지만 저는 다음에 액션을 진행할 Element를 기준으로 잡습니다.  DOM 체크는 간단합니다.

Puppeteer는 단일 요소는 $ 다중 요소는 $$ 로 찾게 되는데 해당으로 체크할시 null 이 아니라면 Element가 DOM 에 는 붙어있다는 의미가 됩니다. 

 

그리고 그다음 isIntersectingViewport 를 호출하면 현재 보여지는 화면에 제가 선택한 Element 가 보여지는지 안보여지는지 확인이 가능합니다.

 

 

모든 페이지가 MPA로 구성되어있다고 해도 현재 브라우저의 뷰 포트에서 노출이 되고있느냐 아니냐로도 체크 되기도 하기 때문에 해당 API 는 지금 보여지는가 에 대한 사용으로 가능할거 같습니다.

 

 

 

감사합니다.

 

안녕하세요 깍돌이 입니다.

 

기존에 준비하였던 내용중 일단 여기까지 하고 배포 릴리즈가 있기 때문에 .. 주말에 작업을 (또르르) 하고 다시 이어서 할 예정입니다.

 

품질은 제품이 잘 나가야 하는게 1순위니까요 이번 LNB Menu 선택 이후에 할 예정된 포스팅은 

  • Selenium-Webdriver 에서 Puppeteer 로 작업중인 이유 (Puppeteer  선택의 이유 )
  • 요즘 새로 핫한 playwright 에 대해 ( 퍼펫티어 팀이 나가서 MS에서 개발 중 ) 
  • MPA, SPA 페이지에 대한 waitForNavigationAPI , isIntersectingViewport API  사용 
  • 현재 진행 중 인 Console 자동화에서 Server 생성 (waitForNavigation API 먼저 포스팅 예정)
  • Server List에서 서버 상태 체크 

등등이 있고 이제까지 업무하면서 개인적으로 공부 했던 부분들을 모두 포스팅으로 남겨 놓도록 하겠습니다.

 

LNB Menu 선택을 위한 함수 제작 입니다.

뭔가 앞에 로그인기능, 콘솔 이동, 파라미터 유효성 등 저렇게까지 해야하나 싶을정도의 예외 처리 등이 들어가게 되는데 이유는  공통적으로 사용되는 기능이라면 모듈화가 필수기 때문입니다. 

 

3개의 Test Case 가 아래처럼 되어있다고 생각해보겠습니다.

 

각각의 로그인을 따로 구현, 콘솔이동도 따로 구현, 메뉴선택도 따로 구현 하였습니다.

이렇게 구현할 경우면 앞선 포스팅처럼 까다롭게 할필요도 없습니다. 단순히 그냥 지금 현상태로 로그인 Element가져와서 바로 입력하고 실행하게 하면 되는 것이지요  그럼 예외처리도 필요없고 파라미터도 필요 없고 하지만 대부분의 케이스들의 UI를 하기 위해선 콘솔로 이동을 하여야 하기 때문에 중복되는 케이스들이 발생합니다. 

 

그래도 사용성에 문제가 없어서 잘 쓰고 있었다면 ?  네 이상이 없으면 상관이없을 수 있습니다. 

하지만 이렇게 진행하다가 Automation Test Case가 100건까지 늘어났습니다. (이때까지 아무 이슈가 없다고 했을 경우)

그러는 와중에 개발팀에서 메뉴 Item의 class를 바꿨습니다.  그럼해야할 작업은

 

"모든 케이스의 직접 .class를 다시 맞춰서 찾아 주는 작업을 해줘야 합니다.  콘솔이동시에 URL체크로 하고있는데 이경우도 마찬가지입니다. 이동하는 URL의 변경 될경우 모두 바꿔 주어야 합니다.  그렇기 때문에 공통되는 작업들에 대해서는 일단 혼자 작업한다고 해도 모듈화를 우선적으로 하는게 건강에 좋기 때문에 위와같이 작성 되고있습니다.

그리고 모듈화에 앞서 UI Automation 에 서 보면 아래와 같은 질문들이 올라오는 경우가 있습니다.

 

"UI Automation 을 하면 개발자들의 자꾸 바꾸는 바람에 테스트가 오류가 납니다."

 

라는 이야기를 많이 듣기도했고 해보다 보니 그런거 같기도 하고 그래서 UI 는 답이없네 라고 할수도 있습니다.

하지만 실제로 경험을 해봤을 경우 웹사이트의 경우 그렇게 크게 바뀌지 않습니다. (대개편이 일어나지않는 이상)

특히 B2B 의 제품을 QA하고 계시다면 더더욱 그럴일은 더 적어지게 됩니다.  

 

"그럼 QA분들이 말하는 게 다 허상이냐" 그건 또 아닙니다. 제가 느꼈던 짤막한 경험으로는 xpath 의 사용 혹은 고정 CSS Selector의 사용이였습니다. ( Element 선택에 대한 기본기가 부족하다고 할수 있습니다. )

 

예를 들면 요소 > 요소 > 요소  에서 최초에 3번째의 요소만 무조건 찾는 형태로 구성되어 있는 상태에서 

개발자들의  hidden Element를 추가하게 될시 ( Tooltip 등을 Custom 으로 제작시등 많이 사용 합니다. - Z-index를 통한 툴팁 박스 ) 

요소 > 요소 > 히든 > 요소 의 구조가 가 되기때문에 기존에 지정해놓은 경로인 세번째 요소로는 찾아지지가 않는 것뿐입니다. xpath 가 나쁘다는 건 아니지만 CSS Selector를 유연하게 작업할수 있다면 그리고 공통적으로 써야하는 모듈을 제작하면서 예외처리를 하나씩 추가하게되면 화면을 통쨰로 갈아 엎지 않는 이상 자주 변경할 일은 발생하지않습니다.

 

그래서 결론적으로는 해당 공통케이스에 대한 모듈화 + CSS Selector를 위한 정확한 Element 선택 을 LNB Menu 에서 사용하고자 합니다.   같이 사용하게된다면 메뉴는 가장많이 사용되는 기능일수 있습니다.

 

 

 

 

 

 

우선 사용 호출 부 입니다.

navigateLNBMenu 호출시 

해당 서버 밑의 서버 메뉴를 선택해야 하기 때문에 Server, Server를 파라미터로 받습니다. 만약에 3depth 까지 있을수 있기에 파라미터는 배열형태로 받게 되었습니다.

 

["Server","Storage"]  라면 Server를 열고 Storage를 선택하게 합니다.

 

지금은 완성이 아니지만 추가작업으로 Server가 ( 1depth ) Open 되어있는상태에서 Server, Snapshot이 들어오게된다면

1depth의 Server가 Expand 상태라면 바로 Snapshot을 선택할 수 있어야합니다.

 

 

 

 

다른 파라미터로는 testplatform 과 environment를 받게 됩니다.  NCP의 콘솔의 경우 Classic 과 VPC를 선택할수 있기 때문이고 environment 는 환경 로깅 용 입니다.

보시면 해당 페이지의 개발자의 경우 모든 첫번째 메뉴들은 a태그와 id값을 사용 하였습니다.

따로 요청 하지않은 경우에 위와같이 id값을 직접 쓰게 되면 매우 좋은 케이스로 자체적으로도 유니크한 값을 쓰고있다고 보시고 #을 이용하여서 가져옵니다.

 

#인 경우는 따로처리할 필요가 없습니다. 다만유의해야 할 점이 있다면 li나 a 태그가 아니라 그 하단에 span 태그를 선택하는점  '#Server > span' 으로 클릭해주면 자연스럽게 열리게 됩니다. 

 

DNS 메뉴를 선택하려고 해도 

 

같은 방식으로 되어있기 때문에 첫번째 메뉴 선택까진 큰 무리가 없습니다.

 

두번째 서브 메뉴를 선택하려고 하면  메뉴상태에 따라서 여러가지 선택이 될수 있는 데 금일은 .active상태인걸 확인하고 (바로 1depth 의 메뉴가 열렸다는 가정) 선택을 할수 있도록 합니다. 

 

지금 작성을 하려고 보니 예외처리나 여러가지 기능들이 잘 안되어있네요.  간단하게 설명드리자면

 

위의 케이스는 active된 케이스 ( Server ) 에 첫번째 Element를 가져와서 선택하게 되어있네요 (이부분은 수정해서 해당 포스팅에 그대로 작성하겠습니다.)

 

상태별 케이스를 추후 추가!

 

 

 

 

 

 

 

 

안녕하세요 Software QA로 일하고 있는 깍돌이 입니다.

해당 포스팅 시리즈는 NCP 를 기준으로 UI Automation 을 하는 과정기를 하나씩 포스팅 하려고 합니다.

모두가 알고 있는 사이트를 기준으로 하는게 좋을거 같아서 선정하게 되었습니다.

 

기존엔 네이버로 했었는데 트래픽이 많이 몰려서 그런가 IP를 막고 그러네요 ㅜㅜ 우회 하는 방법은 찾았지만

 

포스팅으로 써서 좋을게 없을거 같아서 요즘 클라우드 시대에 많은 사람들이 사용하기 시작하는 네이버 클라우드 플랫폼을 기준으로 작성하게 되었습니다.

 

우선 UI Tool 은 구글에서 관리하고 요즘 핫한 Puppeteer로 작성하겠습니다.

기본적인 툴 세팅이나 실행등은 많은 블로그 및 오픈소스 Github에 너무 자료 설명이 잘되어있어서 너무 한땀 한땀은 스킵을 하도록 하겠습니다.~


Puppeteer - Github ( Example 및 API 리스트들 다 있어서 좋습니다. )

https://github.com/puppeteer/puppeteer

 

GitHub - puppeteer/puppeteer: Headless Chrome Node.js API

Headless Chrome Node.js API. Contribute to puppeteer/puppeteer development by creating an account on GitHub.

github.com

 

 

환경 세팅

기본적으로 Node.js  + Type Script + Puppeteer 환경을 기준으로 하기 때문에 기본적인 환경은 작성 하도록 하겠습니다.

package.json

  "dependencies": {
    "axios": "^0.21.1",
    "cloudscraper": "^4.6.0",
    "dotenv": "^8.2.0",
    "express": "^4.17.1",
    "folder-logger": "^1.0.9",
    "form-data": "^2.3.3",
    "moment": "^2.29.1",
    "multer": "^1.4.2",
    "puppeteer": "^5.0.0",
    "puppeteer-extra": "^3.1.12",
    "puppeteer-extra-plugin-stealth": "^2.4.12",
    "request": "^2.88.2"
  },
  "devDependencies": {
    "@types/axios": "^0.14.0",
    "@types/dotenv": "^8.2.0",
    "@types/form-data": "^2.5.0",
    "@types/jest": "^26.0.20",
    "@types/moment": "^2.13.0",
    "@types/multer": "^1.4.5",
    "@types/node": "^14.14.28",
    "@types/puppeteer": "^5.4.3",
    "pm2": "^4.5.4",
    "typescript": "^4.1.5"
  }

tsconfig.json

 

{
  "compileOnSave": false,
  "compilerOptions": {
    "outDir": "./dist",
    "baseUrl":".",
    "paths":{
      "*":[
        "types/*",
        "bin"
      ],
      "dist/*":[
        "../../dist/*"
      ]
    },
    "noEmitHelpers":false,
    "importHelpers": false, // tslib 에서 방출된 헬퍼를 import 합니다. (예. __extends, __rest, 등..)
    "sourceMap": false, //해당하는 .map 파일을 생성합니다.
    "declaration": true,  //해당하는 .d.ts 파일을 생성합니다.
    "inlineSourceMap": false, //별도의 파일 대신 소스 맵으로 단일 파일을 내보냅니다
    "noImplicitAny": false, // any 타입으로 암시한 표현식과 선언에 오류를 발생시킵니다.
    "module": "commonjs",
    "target": "es5",
    "types": [
      "node"
    ],
    "lib": [
      "ES5","ES6","DOM","DOM.Iterable"
    ],
    "moduleResolution": "node",
    "forceConsistentCasingInFileNames": true, //동일 파일 참조에 대해 일관성 없는 대소문자를 비활성화합니다.
    "noImplicitReturns": true, // 함수의 모든 코드 경로에 반환값이 없을 때 오류를 보고합니다.
    "noImplicitThis": true, //any 타입으로 암시한 this 표현식에 오류를 보고합니다.
    "strictNullChecks": true, // 엄격한 null 검사 모드에서는 null과 undefined 값이 모든 타입의 도메인에 있지 않고 그 자체와 any만 할당할 수 있습니다(한 가지 예외사항은 undefined 또한 void에 할당 가능하다는 것입니다).
    "suppressImplicitAnyIndexErrors": true, //인덱스 시그니처가 없는 객체를 인덱싱하는 경우 --noImplicitAny 억제합니다. 오류를 시그니처 자세한 내용은 #1232 이슈를 참조하세요.
    "allowSyntheticDefaultImports": true,  //default export가 없는 모듈에서 default imports를 허용합니다. 코드 방출에는 영향을 주지 않으며, 타입 검사만 수행합니다.
    "esModuleInterop": true,// 런타임 바벨 생태계 호환성을 위한 __importStar와 __importDefault 헬퍼를 내보내고 타입 시스템 호환성을 위해 --allowSyntheticDefaultImports를 활성화합니다.
    "resolveJsonModule": true, // json 확장자로 import된 모듈을 포함합니다.
    "emitDecoratorMetadata": true, // 소스에 데코레이터 선언에 대한 설계-타입 메타 데이터를 내보냅니다
    "experimentalDecorators": true, // ES 데코레이터에 대한 실험적인 지원을 사용하도록 활성화
    "skipLibCheck": true, // 모든 선언 파일 (*.d.ts)의 타입검사를 건너 뜁니다.
    // "strict": true,
    // "alwaysStrict": true,
    "listEmittedFiles": true, // 컴파일의 일부로 생성된 파일의 이름을 출력 
  },
  "include": [
    "./src/**/*",
    "./bin/**/*",
    "./test/**/*"
  ],
  "typeRoots": [
    "./node_modules/@types"
]
}

 

기본적으로  .env 파일을 사용합니다.  ( 필요한 부분만 작성하였습니다. ) 

PUB_REAL=https://console.ncloud.com
DB_MANAGER_SERVER=http://localhost
DB_CONNECT=false

 

main 함수

const main = async ()=>{
  try{
    /** NSTP Test Runner Param Validation */
    const TestConsole:string = paramValidator();
    /*
     Safety Navigate Console
    */
    await LoginConsole();
    switch(TestConsole.toUpperCase()){
      case 'PUB_BETA':
        await PUBLIC('beta');
        break;
      case 'PUB_REAL':
        await PUBLIC('real');
        break;
      case 'FIN_REAL':
        break;
      case 'FIN_BETA':
        break;
      default:
          Logger.info('Non Selected TestConsole');
        return;
    }
    await Puppeteer.close();
  }
  catch(err){
    Logger.error(err);
    process.exit(1);
  }
}

환경별 실행할수 있는 분기 처리문이 포함된 main 함수입니다.  오늘 포스팅에서는 paramValidator LoginConsole을 알아 보도록 하겠습니다.

 

paramValidator

우선 해당 Test-Runner를 실행하기 위해서는 로컬에서 실행하는 목적이 아닌 Trigger서버를 통해서 실행 될 예정입니다.

Express 서버를 통해 실행 될 예정이고 실행은 아래와 같이 진행됩니다.

app.get('/uuid',async(req,res)=>{
  try {
    const uuid = createUUID();

    res.send(uuid);
  } catch (error) {
    console.error(error);
    res.send(error);
  }
});

app.post('/run',async (req,res)=>{
  const {id,pw,env,platform,uuid}  = req.body;
  console.log(`Test Call ${req.body}`);

  const TaskObject = {
    "account":id,
    "password":pw,
    "env":env,
    "platform":platform,
    "uuid":uuid
  }

  console.log(TaskObject);
  runTestRunnerExec(TaskObject);
  res.send(uuid);
})
app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`)
});

 // npm start account passwd pub_beta classic uuid

function runTestRunnerExec(TaskObject){
  let command =`npm start ${TaskObject.account} ${TaskObject.password} ${TaskObject.env} ${TaskObject.platform} ${TaskObject.uuid}`;
  exec(command,(err,out,stderr)=>{
    console.log(out);
  });
}
function createUUID() {
  let d = new Date().getTime();
  if (
    typeof performance !== 'undefined' &&
    typeof performance.now === 'function'
  ) {
    d += performance.now(); // use high -precision timer if available
  }

  return 'task-xxxx-xxxx-4xxx-yxxx'.replace(/[xy]/g, function(c) {
    var r = (d + Math.random() * 16) % 16 | 0;
    d = Math.floor(d / 16);
    return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);
  })
 }

코드의 일부를 가져왔으며 실행시 파라미터가 아래와 같습니다.

 npm start account passwd env platform uuid

UUID의 Controller로 UUID를 발급을 받고 해당 Worker Job(UUID) 로 실행 요청을 하기 때문에 해당 의 형태를 가지고 있어 Test-Runner에서도 한번 유효성 검사를 하는 구간을 두자는 의미에서 작성하였습니다.

 

function platformValidate(param:string) : void {
  const testPlatForm = {
    "FIN_REAL":process.env.FIN_REAL,
    "FIN_BETA":process.env.FIN_BETA,
    "GOV_REAL":process.env.GOV_REAL,
    "GOV_BETA":process.env.GOV_BETA,
    "PUB_REAL":process.env.PUB_REAL,
    "PUB_BETA":process.env.PUB_BETA
  }
  if(param==="undefined" || param===undefined || param===null){
    Logger.error('Please Check your Runtime Parameter ');
    Logger.error(`1. param is -> ${param}`);
    process.exit(0);
  }

  
  let _param:string = param.toUpperCase();
  const url = testPlatForm[_param];

  if(url==="undefined" || url===undefined || url===null){
    Logger.error(`undefined Get URL : ${url}`);
    Logger.error('Check List');
    Logger.error('1. Platform Param ');
    Logger.error('2. Directory Root Check your .env File ');
    process.exit(0);
  }
  process.env.url=url;
  return url;
}
export const paramValidator = ()=>{
  console.log(process.argv[6])
  if(process.env.NODE_ENV==='development'){
      Logger.info('Development Env');
  }
  else{
    if(process.argv.length <= 5){
      Logger.error('Parameter Validation Error');
      Logger.error('Please set [ID], [PassWord], [TargetPlatForm] ');
      Logger.error(`Current Param ID : ${process.argv[2]}`)    
      Logger.error(`Current Param Password : ${process.argv[3]}`)    
      Logger.error(`Current Param Test Console : ${process.argv[4]}`)  
      throw new Error('Param Validation Error');  
    }
    else if(typeof process.argv[6]==="undefined"){
      Logger.error('UUID is not Exist');
      throw new Error('Required UUID Param');
    }
    let ID = process.argv[2];
    var emailReg = /^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*.[a-zA-Z]{2,3}$/i;
    if(!emailReg.test(ID)){
      throw new Error(`ID Email Error is not Email ${ID}`);
    }
    Logger.info(`Automation URL ${platformValidate(process.argv[4])}`);
    
    for(let i=2;i<process.argv.length;i++){
      Logger.info(process.argv[i]);
    }
    // global UUID variable
    process.env.nstpUUID = process.argv[6];
  }
  
  return process.argv[4];
}

간단하게 넘어가자면 5개의 파라미터를 기본으로 받아야하기 때문에 

account - 로그인 할 계정

passwd - 로그인 할 계정의 비밀번호

env  - 환경  ( 네이버 클라우드는 민간, 금융, 공공 3가지의 환경을 가지고 있습니다. ) 

platform - 플랫폼 ( Classic, VPC 의 2가지 플랫폼을 지원 합니다.)

uuid - UUID로 생성되어 발급된 UUID

(* 현재는 해외리전도 있는걸로 보아 해외리전 파라미터도 추가가 되어야겠네요 제대로 작업해서 만든다면요 )

 

1. 파라미터 개수 및 파라미터 확인 

2. account가 email인지 확인 

3. 파라미터에 따른 .env에서 URL을 정상적으로 가져왔는지 확인 

etc. 환경등을 확인하는 조건문 등이 있긴한데 샘플 파일이라고 귀엽게 넘어 가주시면 좋겠습니다. ㅜ

 

오류 화면

 

정상 실행 및 이동&amp;amp;amp;amp;nbsp;

파라미터 유효성 검사가 끝났다면 Console 로그인을 진행 하도록 하겠습니다.

 

LoginConsole

로그인 콘솔입니다. 사실 이렇게까지 해야되나 싶기도하지만 LoginConsole을 로그인 모듈로 빼서 여러 곳에서 호출해서 쓴다고 가정을 하고 작업을 하다보니 꽤 많은 양의 작업이 되었습니다.

 

우선 파라미터로 id,pw를 넘겼기 때문에 process.argv를 통해 파라미터 값을 가져옵니다. ( id,pw의 순서는 고정 ) 

getBrowser를 통해 퍼펫티어의 기본 세팅을 한후 브라우저를 실행합니다.

getBrowser는 API 가 아닙니다.

싱글톤 객체로 사용중인 자체 함수입니다. initialize 는 스킵하도록 하겠습니다. ( 원한다는 요청이 있으면 다시 재포스팅하겠습니다.)

 

그리고 나오는 isLoginPage 일단 해당 설명을 하기전에 NaverCloudPlatform의 페이지이동에 대한 히스토리부터 생각해보겠습니다.

 

보시면 바로 console.ncloud.com 으로 가게 되는데 로그인이 되어있지 않는 상태 의 세션 브라우저를 확인시 순서는 아래와같습니다.

 

1. 로그인창

 

2. 비밀번호 변경 정책에 어긋 났을 경우 

 

 

3. 비밀번호 변경 정책에 어긋나지 않았을 경우 ( 콘솔 페이지로 이동 )

이동하였지만 Dimmed 처리에대한 다시 보지않기 처리가 PC에 세팅되어있지 않은 경우

 

4. Dimmed 처리에 다시보지않기 를 설정한 기록이있어 정상적으로 이동된 경우

 

 

 

여기까지가 로그인 을 하여 콘솔까지의 정상이동으로 볼수 있습니다. 이와중에 해결해야하는 부분은 아래와 같습니다.

 

1. 로그인 정상 처리

2. 로그인 정책 위반시 다음으로 넘길수있는 처리

3. Dimmed 처리 안되어있는 PC의 경우 Dimmed에 대한 처리 

 

로그인 정상 처리

process.argv로 ID, PW를 받아온 상태에서 이제는 입력을 해야하고 입력하기 위한 Element를 찾아야 합니다. 모든 사람들이 Element를 찾는 방법은 수십 수백개라 저는 저만의 방식을 찾은 거를 포스팅하겠습니다.

 

그전에 현재 페이지가 login 페이지가 맞는지 먼저 체크합니다.  ( explicitlyWait도 API 가 아닙니다. 자체 제작한 함수로 일단은 스킵하겠습니다.  해당 함수만 포스팅하려면 한페이지가 작성되어야해서 ;; )

핵심은 selector 부분입니다. 해당 css 셀렉터를 통해서 해당 페이지 유무를 확인합니다.

이미지시에 보이는 셀렉터를 기준으로 잡은 거였습니다.   로그인 페이지는 SPA Web FE 개발자에 잠시 빙의를 해보자면 Login Component를 따로 쓸걸로 예상되기 때문에 해당 페이지는 웬만하면 큰 변화가 없을 것을 기대합니다.

 

그렇다고 Chrome브라우저에 JS CSS 복사 기능을 쓰게 되면 아래와 같이 나타나는데 

document.querySelector("#username")

위처럼 사용해도 될수 있지만 해당 페이지에서 유일하게 placeholder에 아이디* 로 들어가게되는 경우는 input box 뿐이라고 생각되어서 위의 CSS Selector를 사용하였습니다. ( 저는 xpath 는 사용안하고 CSS Selector를 선호합니다.  포스팅하려면 할수있는게 엄청많군요 )

 

 이렇게 현재 페이지가 Login Page라는 점이 확인되면 Inputbox와 Password box를 찾아서 입력 후 로그인 버튼을 선택합니다. ( 이부분은 약식으로 하겠습니다. ) 

 

간단하게 입력 후 저는 Enter 키로 Navigate 를 진행했습니다.

page.waitForNavigation() -> 퍼펫티어의 API 인데 MPA에서는 문제가 되지 않지만 SPA ( Single Page Application ) 에서는 많은 문제가 됩니다. 현재 만들어진 페이지가 Vue, React, Angular 같은 SPA로 만들었는지 확인이 되어야합니다. 아니면 현재 페이지가 서버 라우팅인지, 클라이언트 라우팅인지 명확한 확인이 된 후에 해당 API 써야 합니다. 그렇지 않으면 Timeout이 나게 됩니다. - 이부분도 따로 포스팅을...

 

 

엔터까지 입력이 된다면 이제 해당 사이트의 로그인 정책에 위반되었을 케이스도 확인 하여야 합니다.

로그인 정책 위반시 다음으로 넘길수있는 처리

위에 보시면 이동후에 isDashBoard -> 네 맞습니다. 콘솔 대쉬보드로 이동이 됐는지 체크하는겁니다.

확인내용은 다음과 같습니다. (현재 URL , 이동할 URL) 을 받아와서 비교합니다.

이동하려는 페이지(대쉬보드 URL )가 아니라면 나올수있는 페이지는 비밀번호 정책 페이지 또는 비밀번호 오류 가 있을수 있습니다.

 

1. 대쉬보드 일 경우 ( Successfully )

2. 대쉬보드가 아닐 경우

   2-1 ( passwordAFterwardsCheck ( 정책위반이 될경우 - 비밀번호 변경 ) )

   2-2 ( 그외에 오류 ) - 비밀번호 가 틀리거나 ID를 잘못입력했거나 -> Exception Error

 

 

 

passwordAfterwardsCheck ( 비밀번호 변경 후 90일이 지났을 경우 에 대한 예외처리 )

async function passwordAfterwardsCheck(page:puppeteer.Page,currentURL,navigateURL){
  const selector = '.loginSecure';
  const loginSecureEle = await Puppeteer.explicitlyWait(page,selector);

  /** Check passwordChange Request Page */
  if(loginSecureEle!==false && typeof loginSecureEle !=="boolean"){
    Logger.info(`🚧 The current page is the password change request page. `);
    Logger.info('🚧 Proceed to change the default setting afterwards.');

    let props = await Puppeteer.getProps(page,loginSecureEle,'innerText');
    let regEx = /비밀번호변경 안내|90일마다/gi;
    let changePasswordCheck = regEx.test(props);
    /** Check passwordChange Request Button */
    /** 30일 비밀번호 변경 버튼 유무 확인  */
    if(changePasswordCheck){ 
      Logger.info('[Check] nextChangeButton ');
      const selector = '.loginSecure button';
      const buttons = await Puppeteer.explicitlyWaits(page,selector);

      let nextChangeBtn;
      await Array.prototype.reduce.call(buttons,async(prev,curr)=>{
        const nextItem = await prev;
        let itemText = await Puppeteer.getProps(page,curr,'innerText');
        let diffText = itemText.replace(/\r\n|\n| |\s/gi,"");
        Logger.info(`[diffText] : ${diffText}`);

        if(/다음에변경하기/gi.test(diffText)){
          Logger.info('[Check-Success] nextChangeButton ');
          nextChangeBtn = curr;
        };

        return nextItem;
      },Promise.resolve());
      if(nextChangeBtn==="undefined" || 
      nextChangeBtn===undefined || 
      nextChangeBtn ==="null" || 
      nextChangeBtn===null){ 
        //! nextChange Button is NULL
         Logger.error('[Check-Failed] nextChange Button is not Exist'); 
         return false;
      }
      else{
        Logger.info(`[Click] passwordAfterwards Button`);
        // * Console로 변경 될 때 까지 클릭 
        await Puppeteer.forcedClick(page,nextChangeBtn,"다음에 변경하기",async ()=>{
          try {
            let regExp = /[A-Za-z.-]+/g;
            let currentURL:any = await page?.url();
            let currentProtocolUrl = currentURL.match(regExp)[1];
            let navigateProtocolUrl = navigateURL.match(regExp)[1];
            Logger.info(`[Current] ${currentProtocolUrl}  <----> [Target] ${navigateProtocolUrl}`)

            return currentProtocolUrl===navigateProtocolUrl;
          } catch (error) {
            console.error(error);
            return false;
          }
        });

        //? passwordAfterwardsCheck Success Return  
        return true;
      }
    }

    /** InValid ID or InValid Password */
    else{
      Logger.error('changePasswordCheck is not Exist');
      return false;
    }
  }
  else{
    Logger.info('not Exist loginSecureEle ');
    Logger.info('Login Actions Check');
    const loginRootElement = await Puppeteer.explicitlyWait(page,'.center-wrap.mh-20');
    let resultCheck;
    if(loginRootElement!==false && typeof loginRootElement !=="boolean"){
      resultCheck = await Puppeteer.getProps(page,loginRootElement,'innerHTML');
    }

    if(resultCheck.match(/아이디(메일)를 확인해 주세요/gi) ){
      Logger.error('[Login Actions] ID/Mail Error');
    }
    else if(resultCheck.match(/패스워드 오류/gi) &&
    resultCheck.match(/패스워드 오류 : 0회/gi)){
      Logger.error(('[Login Actions] ID Error'));
    }
    else if(resultCheck.match(/패스워드 오류/gi)){
      Logger.error(('[Login Actions] PassWord Error'));
    }
    else{
      Logger.error('[Login Actions] UnKnown Login Error');
    }
    return false;
  }

}

 

해당의 페이지도 마찬가지로 공통 Component 로 보여지는 페이지 이기때문에 정책변경에 대한 .loginSecure 를 찾습니다. 지금 보니까 article semantic tags를 사용하였기 때문에 article .loginsecure 로 찾는 게 조금 더 유니크 할수 있겠네요

 

forcedClick 같은것도 자체 제작 API 입니다.  해당 코드가 정상적으로 이동되면 이제 콘솔대쉬보드로 이동되면서

Dimmed 처리가 나타나게 됩니다. 

 

Dimmed 처리의 핵심부터 말씀드리자면

 

.coach-mark 입니다.  해당 이 body 역할을 하고 있는 것으로 확인되며 해당 Dimmed 처리 역시 해당 Element Body 를 이용해서 찾은 후  변하지 않는 유니크 값을 찾습니다. 

/환영합니다.|님,|다시 보지 않기/gi;

 

 

async function dimmedCloseActions(page:puppeteer.Page):Promise<boolean>{
  const selector = '.coach-mark';
  const dimmedEle = await Puppeteer.explicitlyWait(page,selector);

  if(dimmedEle!==false && typeof dimmedEle !=="boolean"){

    let diText = await Puppeteer.getProps(page,dimmedEle,"innerText");
    let regex = /환영합니다.|님,|다시 보지 않기/gi;
    await page.waitFor(500);

    if(regex.test(diText)===true){
      Logger.info("🚧 Dimmed is output and Close Window.");
      let dimmedBtnArr = await Puppeteer.explicitlyWaits(dimmedEle,'.btn');
      let dimmedCloseBtn;

      //! dimmedBtnArr 이 undefined 거나 NULL일 경우에 대한 분기 처리 필요 

      await Array.prototype.reduce.call(dimmedBtnArr,async(prev,curr)=>{
        let nextItem = await prev;
        let btnText = await Puppeteer.getProps(dimmedEle,curr,'innerText');  
        let diffText = btnText.replace(/\r\n|\n| |\s/gi,"");
        
        if(/닫기/gi.test(diffText)){
          dimmedCloseBtn = curr;
        }
        return nextItem;
      },Promise.resolve());

        await Puppeteer.forcedClick(page,dimmedCloseBtn,"딤드 처리 제거",async ()=>{
          try{
            let afterDimmedEle = await Puppeteer.explicitlyWait(page,'.coach-mark',3,500);
            if(afterDimmedEle===false){
              return true;
            }
            else{
              return false;
            }
          }
          catch(err){
            console.error(err);
            return false;
          }
        })
      return true;
    }
    else{
      return false;
    }
  }
  else{
    Logger.info('🚧 Dimmed was not output .Process to the next Step .');
    return true;
  }

}

 

저는 여기까지 유니크한 값으로 비밀번호 정책 및 Dimmed처리를 진행하였습니다.

이후에 로그인 및 대쉬보드 이동 테스트를 500번정도 진행하였으나 현재까진 오류가 없었습니다.

 

수정사항이있다면 추가로 관리하겠습니다.

 

 

 

 

 

감사합니다.

 

*** Place Holder  값이 바뀌여서 Login 함수의 수정이 필요하게되었습니다.

 

+ Recent posts