안녕하세요 깍돌이 입니다. 오랜만에 인사드리네요 기존에 작업했던 내용들 ( UI ) 혹은 그 뒤에 작업했던 내용들이

 

사실 거의 다 대외비성이라 이게 .. 참 포스팅을 할수 없다는게 아쉽네요 

 

이번에도 시작은 이렇게 하고 또 대외비로 빠질진 모르겠지만..  나름의 큰 프로젝트를 하고있기도 하고 이번 UI는 있는거 에 한해서 이래저래 조합조합 해서 진행할 예정입니다.

 

PlayWright 입니다. 핫하기도하고 팀내에서 먼저 쓰고 있는 분들이 있기도하고 일단 간단하게 맛만 봤었을때 특이점이 하나 있어서 선택해보게 되었습니다.

 

물론 저는 셀레니움 버전 1 , 버전 2(RC타입) , 퍼펫티어도 어느정도 해봤으니 이래저래 팀내 프로젝트도 진행하고있고 여러 이유를 이용해서 이걸 시작해도 되지만 같이 해야 하는 인원이 있는 만큼  여러 가지 고민중 선택하게 된 계기는 

러닝커브가 매우 낮다 입니다.

 

셀레니움에서 고민해야될 부분 들이 100가지가 있다면 실제로 playwright 에서는 없다고 봐도 무방합니다. (기본적인 트러블 슈팅이 아닌 근본적인 고민 부분에 대한 이야기입니다. ) 

 

대표적으로 Find Element 에 대한 모듈 분리 및 다른 전략들이 필요로 하는데 이중에 예외처리 해야되는 것들 중 자동화 하시는 분들이 어려워하는게 Element 를 찾지 못하는 경우 입니다.  해당 Text 값같은 걸 찾는 경우 

Element에서 inner Text인지 textContents인지 value인지 data-property 인지 매번 예외처리를 해주었어야 했지만 playwright 에서는 해당 부분을 전부 자체 개발된 API 를 통해서 지원해주기 때문에 러닝커브가 매우 낮다고 판단하였습니다.

 

https://playwright.dev/docs/intro

 

Installation | Playwright

Playwright Test was created specifically to accommodate the needs of end-to-end testing. Playwright supports all modern rendering engines including Chromium, WebKit, and Firefox. Test on Windows, Linux, and macOS, locally or on CI, headless or headed with

playwright.dev

 

기본적으로 설치 하고 튜토리얼을 해보면서 Puppeteer와 차이점? 같은 부분을 적고 정리 하는 과정을 적어 보겠습니다.

 

( 솔직히 PlayWright 검색하면 다 누구껄 복붙했는지 다 같은 내용의 블로그 밖에 없네요 ;; 인생 좀 쉽게 쉽게 가고 싶었지만 한땀 한땀 다시 찾아서 포스팅을 시작.. 추후에는 또 어떻게 될지 모르겠지만 ) 

 

설치나 이런것들은 여기저기 많이 있으니까 제외 튜토리얼 대로 

npm init playwright@latest 아래와같이 설치 

타입스크립트 선택

E2E 테스트 파일을 어디에 넣을건가 -> tests

Github Actions  - Yes

수동 실행 브라우저 설치 - Yes

 

 

설치 후 나오는 설명을 보면서 하나씩 다 해보게 되면

npx playwright test 

e2e 테스트를 실행한다고 되어있는데 이게 무슨 기준일까 해서 실행해보니

프로젝트 - tests 폴더에 example.spec.ts 를 실행합니다. 

-> 실행 경로는 playwright.config.ts에서 바꿀수 있지만 이건 나중에 다시 적겠습니다.

 

npx playwright test --ui

UI 모드로 실행하는 명령어입니다.

폴더에 tests - example.spec.ts를 기준으로 나타나게 되네요

 

클릭하는 시점에 어딜 누르는 지도 보여줍니다.

 

로그도 있고 잘만들어진거 같습니다. 당장은 쓸일이 있나 모르겠네요 하면서 경험적인 내용들을 포스팅하겠습니다.

 

npx playwright test --project=chromium

플레이라이트 내부에서는 여러 프로젝트들이 있는데 그 부분들 중에 선택해서 진행할수 있게 하는 명령어입니다.

TS다보니 해당 리스트가 type으로 표현되어있는데요 

 

수많은 장치들이 있고 맨 밑에 저희가 대부분 써야 할 

데스크탑 앱이 있네요 저게 다 되는지 어떻게 차이가있는지 그냥 해상도로만 구분한건지는 좀더 확인해봐야 할거 같습니다.

 

npx playwright test example

파일을 선택하여 실행한다고 되어있는데 

도대체 example을 뭘로보고 실행하는건지 이해가 되지않았습니다.  파일명의 spec.ts를 무시하고 실행한다 해도

changeexample로 제가 바꿔놨기 때문입니다. 그래서 몇번의 테스트를 해보았는데

 

일단 npx playwright test 명령어를 치게되면 config의 testDir을 기준으로 바라 보게 됩니다.

그래서 1차적으로 해당 폴더의 위치를 하게 되고 파일명을 변경해가면서 테스트를 해보니 

 

demo-todo-app.spec.ts에서

 

demo 도 되고 todo 되고 app 되고 - 도 됩니다. 결론은 like 검색으로 포함되어있다면 해당 파일을 실행하는거같습니다.

둘다 demo를 넣고 실행하게되면  아래의 사진은 demo가 먼저 걸리는 파일을 실행하는거같은데 이 순서를 바꿔보겠습니다.

순서를 바꿔서 1_demo 로 앞에 오는 것을 기대하고 테스트를 해보았으나 기존과같이 밑에 demo-todo를 실행하게 됩니다.

1_demo ( 케이스 2개 )

demo-todo ( 24개 ) 

조금 이상함을 느껴서 npx playwright test로 전체 테스트를 돌렸으나

전체가 돌아가지 않음을 확인했습니다. 

그래서 다시 이상함을 느끼고 확인해보니

.spec 이 들어가야 같이 돌아가는걸로 보여 다시 재 테스트를 하였습니다.

 

다시 demo를 공통으로 넣으니

 

다 돌아가는거 보면 demo가 포함되는 모든 것들이 돌아가는걸로 보입니다.

 

마지막 테스트 ppap 로변경하고 실행

 

 

tc-seocnd.spec.ts로 변경하고 직접 실행

spec을 제외하고 직접 실행

 

결론 : spec이 들어가야 실행이 가능 

npx playwright test --debug

디버그로 실행하여 한땀한땀 실행하면서 내용을 확인할수 있게 됩니다.

Resume을 누르면 그냥 통으로 실행하고 다음 스텝으로 넘어갑니다.  Step over 는 해당 테스트 안에서 한스텝씩 넘어 갈수 있습니다.

 

npx playwright codegen

말 그대로 코드젠입니다. 내가 하는 액션들을 코드로 간단하게 먼저 뽑아주는 역할을 합니다.

 

npx playwright test

 

 

여기까지가 기본 OVER VIEW였습니다.

 

포스팅 시간이 짧아서 OVER VIEW 2탄으로 돌아오겠습니다.

 

2탄 내용은

 

playwright.config 옵션에 대해

 

github action 연결 

 

내부 test모듈 구조 

 

왜 이 틀을 구조가 강제화되어있는가

 

worekr 구조 

개인적인 궁금증 해결 입니다.

 

 

 

 

 

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

 

웹 자동화를 하기 위해서 사용되어지는 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 함수의 수정이 필요하게되었습니다.

 

안녕하세요 Selenium - Web Driver ( Node.js Version ) 에서 SendKeys 에 대해서 간단하게 알아 보려고 합니다.

 

처음에 SendKeys 에 대해서 사용하게 되는 경우 대부분  자동화 도중에 어떠한 값을 "입력" 하게 되는 경우에

 

사용하게 되면서 시작 하게 될 것 같습니다.

 

// DOM select targetElement
let targetElement = driver.findElement(By.css('div .target'));

// targetElement input String 
await targetElement.sendKeys('Automation Input');

하지만 위와같은 방식 말고 다른 방법으로도 사용이 가능합니다.

 

Automation Copy and Paste with SendKeys 

// DOM select targetElement
let targetElement = driver.findElement(By.css('div .target'));

// targetElement input String 
await targetElement.sendKeys('Automation Input');

// Copy and Paste One Take
await targetElement.sendKeys(Key.CONTROL,'a'); // All Select String
await targetElement.sendKeys(Key.CONTROL,'c'); // Block String to Copy 
await targetElement.sendKeys(Key.CONTROL,'v'); // Paste

뭔가 해당 사항만 봤을 때는  'Automation Input' 이 입력되면 Automation Input 이 들어가고

앞에 Control 값이 있을때 해당 값과 키 값을 같이 누르는 것처럼 보입니다.

 

하지만 실제로는 어떻게 동작하는지 알면 왜 위와 같이 동작하는지 이해하기 편합니다.

 

위의 예시코드는 WebElement.sendKeys 였지만 실제로는 Actions 객체로도 가능한데요

이번 포스팅에서는 WebElement.SendKeys 지만 Actions의 SendKeys 도 비슷한 부분들이 많아

우선 Actions의 SendKeys  부터 간단하게 열어 보았습니다.

 

// Selenium github 

https://github.com/SeleniumHQ/selenium/blob/master/javascript/node/selenium-webdriver/lib/input.js#L572

Selenium 은 오픈소스 이다 보니까 코드 가 공개 되어있는데요

 

SendKeys 의 구현체를 발췌 해 보았습니다.

sendKeys(...keys) {
    const actions = [];
    for (const key of keys) {
      if (typeof key === 'string') {
        for (const symbol of key) {
          actions.push(
              this.keyboard_.keyDown(symbol),
              this.keyboard_.keyUp(symbol));
        }
      } else {
        actions.push(
            this.keyboard_.keyDown(key),
            this.keyboard_.keyUp(key));
      }
    }
    return this.insert(this.keyboard_, ...actions);
  }

1. sendKeys(...keys) -> ...keys 로 spread 연산자 입니다. 들어오는 모든 키들을 파라미터로 받습니다.

   이부분이 이해가 안될수 있는게 

function sendKeys(keys){
	console.log(keys);  // automation
}
sendKeys('automation');

이와같은 형태 아닌가? 라고 잠깐 착각 할수 있습니다. 하지만 Spread Operator 를 사용하게되면 조금 이야기가 달라 질 수 있습니다.

function sendKeys(...keys){
	console.log(keys); // ['automation']
}
sendKeys('automation');

위와의 차이점은 배열(Array) 로 들어온다는 점입니다. 이 뜻은  

A, automation 과같이 입력이 된다는 뜻이 됩니다.

function sendKeys(...keys){
	console.log(keys); // ['CONTROL_A','automation']
}
sendKeys('CONTROL_A','automation');

미리 정의해둔 컨트롤(Control) 이나 쉬프트 (Shift) 값이 들어왔을때 조합키 형태로 사용할 수 있도록 

 

여기에서 잠깐 보고 가야 할 부분이 위에 Key.CONTROL 은 lib/input 에 선언이 되어있기 때문에 사용하기 위해서

 

코드의 최상단에

const { Key } = require('selenium-webdriver/lib/input');

로 가져오신 후 사용하시면 됩니다.

/lib/input 선언부

위와같이 되어있기 때문에 사용하고 싶은 부분들을 사용하시면 됩니다.

 

이제 SendKeys 에서 입력 키들을 배열로 받을수 있도록 처리가 되어 있습니다. 

 

간단하게 CONTROL 만 선언해서 돌려 본다면

 

var Key = {
	CONTROL : '\uE009'
}
function sendKeys(...keys){
   const actions = [];
	 for(const key of keys){
		if(typeof key==='string'){
	     	for(const symbol of key){
				console.log(`symbol ${symbol}`);
            }
        }
		else{
		   console.log(`not Symbol ${key}`);
        }
    }
}

sendKeys(Key.CONTROL,'automation');

테스트 결과

보시면 CONTROL 부분은 심볼로 처리되고 그 외에 있는 문자열들은 따로따로 나눠서 되어있습니다.

미리 선언해놓은 심볼들은 내부적으로 다시 연결하여서 그에 맞는 조합키를 입력 합니다. (해당 내용은 이번 포스팅에서는 제외하고 하겠습니다.) 

 

그리고 해당 값들을 통해서 KeyDown 과 KeyUp을 사용하여 진행합니다.

 

"Automation" 이 파라미터로 넘어간다면

 

A Key Down 

A Key Up

u Key Down

u Key Up 

.... 이 순서로 진행이 된다고 볼수 있습니다.

 

그래서 테스트 해 보았습니다. 

 // 첫번째 요소에 Shift 키와 함께 
 await targetElement.sendKeys(Key.SHIFT,'automation');
 // Result : AUTOMATION
 
 // 두번째 요소에 소문자 입력시 
 await subtargetElement.sendKeys('automation');
 // Result : automation
 

위처럼 현재 Element 에 조합키 + 소문자 입력후 다른 Element 에 automation 만 입력시 어떻게 될지 확인해보았을 시

 

입력중인 위젯에 대해서는 Shift키 심볼로 인해 keyDOWN 상태가 유지되어 대문자로 나오지만

 

새롭게 지정된 요소에서는 해당 키 내용이 없기 때문에 소문자가 그대로 나타납니다.

 this.sequences_ = new Map([
      [this.keyboard_, []],
      [this.mouse_, []],
    ]);
	
    insert(device, ...actions) {
    this.sequence_(device).push(...actions);
    return this.sync_ ? this.synchronize() : this;
  }

위의 sendKeys 마지막 부분 요소에 보면 insert하는 부분이 있게 되는데요 위의 코드는 Selenium 자체코드입니다.

 

Actions.SendKeys 의 경우 관련된 액션과 키 값들을 정리 후 한꺼번에 리턴합니다.  

 

Actions의 경우 perform 이 호출될시 리턴된 액션들을 순차적으로 실행하는 형태이기 때문에 그때 순차적으로 실행됩니다.

 

WebElement의 SendKeys 의 경우

 

async sendKeys(...args) {
    let keys = [];
    (await Promise.all(args)).forEach(key => {
      let type = typeof key;
      if (type === 'number') {
        key = String(key);
      } else if (type !== 'string') {
        throw TypeError('each key must be a number of string; got ' + type);
      }

      // The W3C protocol requires keys to be specified as an array where
      // each element is a single key.
      keys.push(...key.split(''));
    });

    if (!this.driver_.fileDetector_) {
      return this.execute_(
          new command.Command(command.Name.SEND_KEYS_TO_ELEMENT)
              .setParameter('text', keys.join(''))
              .setParameter('value', keys));
    }

    keys =
        await this.driver_.fileDetector_.handleFile(
            this.driver_, keys.join(''));
    return this.execute_(
        new command.Command(command.Name.SEND_KEYS_TO_ELEMENT)
            .setParameter('text', keys)
            .setParameter('value', keys.split('')));
  }

위와 같은 형태를 띄고있는데요 Actions 과 유사한점이 있지만 다른점이 있다면 바로 Command 를 통해서 실행한다는점입니다.

눈에 띄는 것은 맨 첫줄에 Promise all 부분인데요 들어오는 키 형태중에 조합형태로 Promise 를 리턴하는 경우가 있을 경우에 대비해서 만들어 놓은 것으로 보입니다.

 

위에서 Shift를 눌러놓은 상태에서 automation 시 AUTOMATION 으로 나온후 타 요소에 갈땐 다시 SHIFT가 없는 상태였는데요

 

한 요소에서 같이 될 경우

        await targetElement.sendKeys(Key.SHIFT,'automation');  // AUTOMATION
        await targetElement.sendKeys('ppap'); // ppap

위와같이 보시면 Commander에서 실행 후 다시 초기화가 되는형태로 보여집니다.

 

마지막 궁금했던 테스트입니다.

 await targetElement.sendKeys(Key.CONTROL,'a',Key.CONTROL,'c',Key.CONTROL,'v','v');
 
 await targetElement.sendKeys(Key.CONTROL,'a','c','v','v');
 

그럼 한줄로 그냥 할수 있을거같은데 어떻게 동작하나 보았습니다. 

실제로 이번에 포스팅한 계기는 복사 붙여넣기 자동화 케이스를 만들다가 작성하게 된 경우입니다.

그렇기 때문에 on-copy 와 on-paste 의 이벤트가 Callback 되는지에 중점을 두게 되는데

 

위의 2케이스 전부 콜백은 동작합니다.  v를 한번 더 넣은 경우를 붙여넣기가 블록상태에선 블록만 해제되니까 테스트용으로 두번 넣었습니다. (붙여넣기 확인용)

 

1번 케이스의 경우 결과가 c 로만 나타나게 됩니다.

Ctrl 누르고 a -> 전체 블록 지정    ( 중요하게 보아야 할점은 KeyDOWN  이라는 점 )

다시 Ctrl 누르고 c  ( 한번 더 눌렸기 때문에 keyDOWN Ctrl 이 UP이 되고 c만 입력 )

다시 Ctrl 누르고 v  ( 다시 누르면서 붙여넣기 )

 

결과 : 몇몇 Text 관련 Element 에서는 focus를 주고 난 후에 입력이 되는 경우가 있어서 저는 위처럼 focus를 주고 입력합니다.   

 

긴글 읽어주셔서 감사합니다!!

  // focus in Textarea
        await this.driver.executeScript('arguments[0].focus();',targetElement);
        await targetElement.sendKeys(Key.CONTROL,'a','c','v');

 

안녕하세요 


자사에 있는 제품중인 UI 제품군 중 TOP라는 웹 프론트앤드 프레임워크 의 QA를 맡고 있습니다.


우선 자동화에 대한 설명에 앞서 TOP 에 대한 설명을 제가 직접 할순 없기에 오피셜로 있는 링크를 올립니다.



해당 TOP 에 대한 공식적인 소개

https://tmaxos.com/index#TOP

(소개 페이지였었지만 지금은 TOP로 개발한 홈페이지가 되었습니다) 


기본적으로 Tool + web framework 형태인데


오늘 포스팅 할 내용은 web framework 에 대한 자동화 이야기 입니다. 


IDE라고 하는 tool 자체에 대한 자동화 이야기는 다음에 작성 할 예정입니다.


비즈니스 프레임워크 중에 는 이례적? 이라고 해야할까요 최신 웹 프레임워크의 트렌드를 가진 프레임워크 입니다.

(SPA,Router,Controller,CSR... 등등)


또한 사용자가 만들어야 할 html 코드를 최소화 하여 Data 형태 만  정하게 되면 화면에 나타날수있게 도와줍니다. 


해당 프레임워크에서는 위의 링크와 같이 화면을 구성하기 위한 수십개의 컴포넌트와 컨테이너 등을 제공하게 되는데


그럼과 동시에 해당 컴포넌트들은 크로스 브라우징 형태로 제공이 되기 때문에 지원하는 브라우저가 많아지는 경우


중복하여 확인하여야 하는 작업 이 너무 많아지게 됩니다.


그렇기 때문에 해당 테스트를 자동화 하기 위한 Selenium WebDriver(Node.js)  Jenkins 를 사용하여서 하나의 


Selenium 프로젝트에서 여러 운영체제 및 브라우저(Chrome,FireFox,Internet Explorer, Opera, IOS Safari, Android ) 에 


대한 테스트를 자동화를 하여야 합니다.



이와 관련하여서는 기존에 포스팅 하였던 자료가 있지만 좀더  구조적으로 설명 하며 개요 부터 설명 하도록 하기위해 


다시 작성하였습니다.


// 기존 자동화 링크 

https://ipex.tistory.com/entry/Jenkins-%ED%85%8C%EC%8A%A4%ED%8A%B8%EB%A7%81%ED%81%AC-%EC%A0%A0%ED%82%A8%EC%8A%A4-%EC%97%B0%EB%8F%99-3-%EB%B2%88%EC%99%B8-Iterative-Test-Build-Steps-%EC%82%AC%EC%9A%A9%EC%97%86%EC%9D%B4-%ED%85%8C%EC%8A%A4%ED%8A%B8%EB%A7%81%ED%81%AC%EC%97%90-%EC%BC%80%EC%9D%B4%EC%8A%A4-%EC%8B%A4%ED%96%89-TestCase-Execution-into-TestLink-without-Iterative-test-build-steps-on-JENKINS



프론트 프레임워크와 기존 웹 페이지 자동화의 차이점


  기존의 화면 (페이지가 만들어져 있는) 에 대한 자동화와 프론트 프레임워크 에 대한 자동화는 차이가있습니다.


            화면 자동화의 경우 A페이지 (로그인 페이지) 일 때




아이디 창에 대한 E2E 테스트가 진행된다고 하면 개발할때 의 스펙에 맞게만 테스트가 진행되어야 합니다.


ex : 50자 최대 일 경우 (50자이상입력이 되는지 )

ex : id만 입력 후 로그인 시 "비밀번호를 입력해주세요" 메시지가 하단에 출력되는지 .

ex : 아이디 입력 후 "엔터" 키 입력 시  "비밀번호" 창으로 이동이 되는지 


해당 화면은 "로그인페이지" 에만 존재하기 때문에 해당 케이스 또한 로그인 페이지에서만 유효한 케이스라고 볼수 있습니다.


하지만 "아이디 입력창" + "비밀번호 입력창" + "로그인 버튼" 이 한개의 컴포넌트라고 보게 됐을경우


<naverLogin-tag> 와 같은 형태로만들어지는 컴포넌트라면 이야기가 달라집니다. 

get함수를 통해서 현재 id pw 가 {id: "",pw:""} 받을수도 있고 하나의 컴포넌트로서 기능적인 면이 들어가기 때문입니다.


컴포넌트를 테스트하는 것과 웹 페이지를 테스트 하는 것은 다르다 


TOP에서는 이런 형태로 수십개의 컴포넌트를 제공하고 있습니다. 단 하나의 태그와 원하는 데이터 형태 로 


사용자가 작성하는 html 태그가 최소화 되는 형태로 제공됩니다.


그렇기 때문에 QA 입장에서는 위젯, 레이아웃,컨테이너가 있을 경우 각각 의 모든 케이스가 TC(테스트케이스) 가 됩니다.


레이아웃은 위젯을 배치하는 컴포넌트이고 위젯은 레이아웃에 배치되는 컴포넌트 컨테이너는 데이터를 표현하기 


위한 컴포넌트 라고 봤을때


A레이아웃  + B 위젯  + C 컨테이너만 놓고 봐도 세개의 컴포넌트에서만 케이스가 무수하게 나옵니다.


ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ


A레이아웃 안에 C컨테이너를 배치시 화면에 렌더링 되는지

A레이아웃 안에 B위젯을 배치시 화면에 렌더링 되는지

A레이아웃 안에 B + C 컴포넌트가 같이 있을시 같이 렌더링이 되는지

.....


ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ


그런데 문제는 하나하나 다 컴포넌트 형태이기 때문에 


각각이 제공하는 API 가있고 속성이 있습니다. 


A레이아웃 - C 컨테이너로 테스트하는데 


C컨테이너에 자체 속성이 20개 API 가 20개 일 경우 단순 무식하게 TC(테스트케이스) 를 만든다고 하면 이 숫자 또한 만만치 않게 됩니다.


그래서 원칙적으로는 페어와이즈 테스트 기법을 통해서 케이스를 추출 + 사용자들에 의해 발생하는 회귀테스트 를 우선 작성하고 이에대한 자동화를 진행하게 됩니다.




SeleniumWebDriver (Node.js) 를 사용하는 이유


  Selenium Webdriver Node.js 를 사용한 이유는 우선적 으로 말씀드리자면 JavaScript를 좀 더 많이 쓰기 때문이였습니다. 

하지만 단순히 이런 이유로 사용하기에는 자동화를 하기 위해 좀더 찾아 본 결과로는 SeleniumWebDriver 를 최초의 만들었던 제작자 가 기존의 JAVA 등의 버전에서 현재 js진영으로 넘어와서 새로 프로젝트를 하고 있기 때문이다.(링크를 하려 하였으나 아직 못찾아서 찾는대로 링크하겠습니다.)



[셀레니엄hq 공식 홈페이지]

https://www.seleniumhq.org/download/



Node.js 의 진영은 보시다시피 메이저 버전이 업된 4.0.0 alpha버전임을 확인 할수 있으며  npm 다운로드 수 또한 매우 높아 해당 셀레니움으로 사용하게 되었습니다. 결론적으로 사용하게 된 이유는


정리를 하자면 

  1. 가장 버전이 높은 (활발하기때문)

  2. 사용하던 js언어와 호환성

  3. Node 에서 할수 있는 기능 (xml포매팅 , 파일 관리 등) 을 같이 사용 가능 (리눅스,윈도우 세팅의 편리)

  4. E2E 에서 여러 브라우저를 직접 실행하여 사용가능 ( 하나의 테스트프로젝트를 통해 여러 브라우저에서 테스트 가능 ) 


이외의 알아본 lib 들은 


phantom.js && capser.js , zombie.js slimer.js , webdrivero.io, Cucumber.js , protactor, 

nightwatch.js htmlUnit,Guitar,Cypress.io,uitest, uirecorder,testCafe(*typeScript) 


정도 나열할수 있지만 이러저러한 이유로 사용하지 않았습니다.

 (기본 베이스가 셀레니움이거나, 크로스브라우징에 적합하지않음 등등)



[NPM 다운로드 수 및 이슈]

위의 사진만 비교해봐도 (2018-11-05) selenium-webdriver를 선택 한 점을 찾을수 있다.



SeleniumWebDriver 와 Jenkins 를 사용시 이점 


정확히는 소스코드를 Git으로 관리하게 되는데 이것을 이용한 사용에 대한 이점도 충분히 있습니다.


테스트 자동화 라는게 단순 자동화 를 해주는 SeleniumWebDriver만 이 아니라 일련의 작업

( 최신의 테스트 코드로 테스트를 진행 하고 결과를 리포팅 하는 작업 까지 )


자동화 프로세스




SeleniumWebDriver 코드 작성 



isDisplayDOM 사용 


Chrome브라우저의 경우 속도가 매우 빠릅니다. 타 브라우저에 비해서 그렇기 때문에 단순히 DOM Select 하고 클릭하고

다른 작업으로 넘어 가도 문제가 없지만 IE의 경우에는 이속도가 보장이 되지않는 경우가 많습니다.

(자동화 작업중 트러블 슈팅은 거의 IE에서 일어납니다. ㅜ) 


그렇기 때문에 자체적으로 테스트할 위젯 (클릭하거나 조작할 DOM Element) 를 가져올때 isDisplayDOM 이라는 함수를 만들어서 사용하게 됩니다. 




let startIdx=0;
let retTarget = await this.isDisplayDOM(id);
let tbody = await retTarget.findElement(By.css('table > tbody'));
let totalRowArray = await tbody.findElements(By.css('tr'));


isDisplayDOM 구현체


구현체는 생각보다 단순합니다. driver.wait 라는 API 를 이용해서

until(Util 관련 모듈 입니다.) 을 이용하여서 elementLocated  대상 element가 브라우저 DOM에 붙을때까지 기다립니다.


뒤에 maxWaitTime 같은 경우는 QA가 최대 대기 시간을 작성해놓습니다. 저는 2초 정도로 하였습니다. 2초안에 DOM 에 붙지 않는 다면 해당 테스트 케이스 및 시나리오는 실패로 간주합니다.

(2018-11-20 ) 해당 함수에는 아직 selector 처리가 없네요 id또는 tag , selector로 가져올수 있기 때문에 이부분에 대한처리도 같이 해줘야

진정한 모듈함수 


topqa.prototype.isDisplayDOM = async function(targetId){
let maxWaitTime = 2000;
try{
// let target = await this.driver.findElement(By.id(targetId));
let target = await this.driver.wait(until.elementLocated(By.id(targetId)),maxWaitTime);
if(targetId === await target.getAttribute('id')){
console.log("checked DOM is : ",targetId);
return await target;
}
else{
return false;
}
}
catch(err){
console.warn("isDisplayDOM Error : ",targetId)
console.error(err);
}
}; // isDisplayDOM



해당 과 같이 작성해서 사용하게 되면  DOM 뜨고 나서 진행되기 때문에 중간에  DOM 을 캐치하지못하여 종료되는 오류는 막을수 있습니다.




해당 최신 코드는 


https://github.com/lgance/selenium_tmax/blob/master/topqaModule/autoManager.js


에서 보실수 있습니다!! ( 2019년 에 퇴사를 하여 최신화를 진행하진 않습니다.)





안녕하세요 셀레니움 JS를 쓰다보면 단순하게 문제가 되는 부분이 있습니다.

 

모든 API 들이 전부 비동기 방식이기 때문인데요

 

A,B,C의 WebElement를 가져올건데

 

findElements 로 가져오게되면

 

Promise <Pending> (Array) 로 가져오게 됩니다. 

 

만약에 가져와서 A,B,C에 대한 작업을 순서대로 해야한다면?

 

let webElements = await driver.findElements(By.css('.webElements'))

 

로 가져온 후에   (사실 가져오는 예시도 await가 붙어있어 망정이지 

 

그냥 let webElements = driver.findElements(By.css('.webElements')) 로 하게되면 이 또한 동작하지 않습니다.

 

Promise<pending> 값이 나옵니다.

 

순환하면서 작업한다고 할시

 

webElements.map( (item,index)=>{

 

});

 

 

webElements.forEach( (item,index)=>{

 

});

 

결과는???

 

X 동작하지않습니다.     driver.findElements 의 반환값이 Object이기 때문이죠

 

프로토타이핑을 합시다.

 

Array.prototype.map.call(webElements, (item,index) =>{

});

Array.prototype.forEach.call(webElements,(item,index) =>{


});


결과는?

넵 동작합니다. 하지만 돌려보시면 알겠지만 문제가 하나 생깁니다.

 

console.log("Before");

Array.prototype.map.call(webElements, (item,index) =>{
        // do thing
});

console.log("Start");

시에 해당 작업의 결과는 Before -> Start -> do thing 이 됩니다.

 

아 ! 안에서 async 하게 해줘야 되는데 그쵸?

 

 

console.log("Before");

await Array.prototype.map.call(webElements, async (item,index) =>{
        // await do thing
});

console.log("Start");


결과는요??

Before -> Start - > await do thing 입니다. 

 

왜인가 하면

 

async 함수와 await는 잘 썻습니다

 

하지만 이 반복 자들이 매번 반복 할때마다 반환되는 Promise에 대한 then 처리가 없기 때문이에요 

 

함수를 async 하게 해서 내부적으로 await 하는건 내부적으로 반환된 item 에 대한 비동기 API 사용시 해당 결과를

 

then 하는 것일 뿐 실제로 저 반환되는 item에 대한 Promise가 없기 때문이죠  (프로미!)

 

해결법이 있습니다.

 

ES9 에서 나온 해결법 부터 말씀드리겠습니다.

for await of 문입니다.

console.log("Before");    for await (const item of webElements){             // do thing [item]    }  console.log("Start"); 

 

결과는요??

 

Before ->  await do thing -> Start 입니다. 

 

드디어 잘나오네요 for await of문은 비동기 Iterator 를 위해서 매 순환마다 Promise로 받기 때문에

 

순서 보장 및 동기적 보장을 해줍니다. 하지만 ES9기준이니 Node에서는 10버전 부터 사용 가능합니다.

 

 

reduce를 활용합니다.

 console.log("Before");  await Array.prototype.reduce(webElements,async (acc,item,index)=>{            const nextItem = await acc;           return nextItem;  },Promise.resolve());  console.log("Start");  

 

 

 

리듀스의 특징 중 순환 마다 누적값?을 받아오고 초기값을 지정할수 있다는 점이 있는데요

 

이 특징을 이용해서 초기값을 Promise.resolve() 로 받게 됩니다. 그러면 첫번째 초기값을 리졸브로 시작하며

 

인덱스를 받게 되는데

 

그떄 await로 받으면 (then 으로 받아도 됩니다.)

 

webElement, (acc,item,index)=>{
        acc.then( (nextItem)=>{

        })

 .catch( (err)=>{console.error(err);};

},Promise.resolve());

 

와 같은 느낌으로 받을수도있지만 결국 또 콜백 헬;;; 

 

그렇기 때문에 위의 async await를 쓰도록 합시다.

 

위의 상황을 다시 한번 말씀드리면 첫 인덱스는 Promise.resolve 로 받게 되고

 

인덱스 아이템이 넘어오는 Promise를 await acc 로 기다리면서 받습니다. (동기적)

 

그리고 그 내부 코드에서 비동기 코드는 다시 await를 쓰고 아닌경우는 평소와같이 씁니다.

 

그리고 결과 값을 다시 누적하기 위해 던져줍니다.

 

reduce에 대한 자세한 설명은 밑의 블로그에 있는데

정말  설명이 끝내줍니다. 

// 제로초 블로그 

https://www.zerocho.com/category/JavaScript/post/5acafb05f24445001b8d796d

 

 

 

 

 

 

 

 

 

 

 

같은 코드로 IE11을 테스트할때 UnsupportedOperationError 에러가 나타납니다.


[package.json]

iedriver: 3.9.2

selenium-webdriver: 4.0.0-alpha.1



[Node.js] 10.8.0

[npm ] 6.2.0


IE Browser - IE 11

해당 에러가 났던 코드입니다.


let subMenuList = await this.currlnbMenu.findElements(By.css('ul li'));

await Promise.all(subMenuList.map(async (item,index)=>{
let topSubMenuTextTag = await item.findElement(By.css('.top-menu_text'));

// 이부분에서 에러가 나타납니다.
let targetText = await topSubMenuTextTag.getText();
// 이부분에서 에러가 나타납니다.
if(clickSubMenuText.toLowerCase()===targetText.toLowerCase()){
console.log("find It");
this.currlnbSubMenu = item;
this.currlnbSubMenuText = targetText;
await topSubMenuTextTag.click();
return item;
}
else{
console.log("[subMenuSelect] Looking for element : " ,clickSubMenuText );
console.log("[subMenuSelect] Checking for element : " ,targetText );
console.log('');
}
}));


정확히 나는 부분은 Promise.all 안에 async map 순환 까지는 문제가 없으나 


순환되는 item 파라미터로 


item.getText , item.getAttribute('innerText') 등의 작업을 하게되면 위의 알수없는 미지원에러가 나타납니다.


그래서 해당 코드를 수정했습니다.


let subMenuList = await this.currlnbMenu.findElements(By.css('ul li'));
let length = subMenuList.length;
for(let i=0;i<length;i++){
let topSubMenuTextTag = await subMenuList[i].findElement(By.css('.top-menu_text'));
let targetText = await topSubMenuTextTag.getText();
if(clickSubMenuText.toLowerCase()===targetText.toLowerCase()){
console.log('find It');
this.currlnbSubMenu = subMenuList[i];
this.currlnbSubMenuText = targetText;
await topSubMenuTextTag.click();
break;
}
else{
console.log("[subMenuSelect] Looking for element : " ,clickSubMenuText );
console.log("[subMenuSelect] Checking for element : " ,targetText );
console.log('');
}
}


// Promise.all 과 map 의 합작으로 인해서 오류가 나는것으로 추측 하여 일반 for 문으로 변경하여 사용


안녕하세요 네이버 홈페이지가 변경되었습니다.


기존의 예제로 제공하던


// 네이버 로그인 자동화

http://ipex.tistory.com/entry/Selenium%EC%9B%B9%EC%9E%90%EB%8F%99%ED%99%94-Naver-Login-%ED%8E%98%EC%9D%B4%EC%A7%80-%EB%B3%80%EA%B2%BD%EC%97%90-%EB%94%B0%EB%A5%B8-%EC%98%88%EC%A0%9C-%EB%B3%80%EA%B2%BD-%EC%95%88%EB%82%B4?category=770641


가 동작하지 않게 되었습니다.


혹시나 하는 마음에 돌려 보았으나




음 실패 합니다. !!


예전에 작성된 코드를 확인 해보니


// input값 가져옴
let inputId = await driver.findElement(By.css('input#id'));


여기서 못찾는거 같아요 왜냐!

변경되었거든용 페이지가 js로 확인해봐도 안보이는점이 확인되었네요 


그러면 로그인 하기전에 앞에 NAVER 로그인 을 눌러줘야 될거같아요 


눌러주는거야 어렵지 않으니 눌러 줍시다.


document.querySelector("div#account .lg_local_btn").click();  (NAVER 로그인 버튼)


로 누르면 되네요


그 다음 페이지에 id와 pw를 입력 하면 될거같아요 


주석 관련해서는 전부 지우도록 하겠습니다.


로그인 버튼만 해서 넘어가고 나서는 기존의 있던 id 와 pw submit Button 을 그대로 사용한거 같습니다.


코드 그대로 사용해도 되겠네요


앗.. 네이버에서 


비밀번호 자동방지 입력 공간을 만들어서 더이상은 진행이 안될것 같습니다.


CAPTCHA

를 도입했네요 네이버에서 


위키 - 

CAPTCHA는 HIP 기술의 일종으로, 어떠한 사용자가 실제 사람인지 컴퓨터 프로그램인지를 구별하기 위해 사용되는 방법이다.


라고하네용

이것은 기존의 텍스트와 이미지를 일그러뜨린 형태로 변형한 후 인식 대상이 변형된 이미지로부터 기존 이미지를 도출해 낼 수 있는지를 확인하는 방식의 테스트이다.


현재 저로서의 능력 밖에 문제에 직면했네용;


자동방지 이미지는 


document.querySelector("img#captchaimg");


로 가져올수 있고 해당 이미지의 src의 뒷부분에 보면 key 값이 있습니다. 이 key 값을 입력해줘야지 로그인이 될거같았


는데 그것도 아닌거같네요



새로고침 시 마다 & 뒤에 숫자가 계속 붙네요 ㅜ



예제를 다른 걸로 .. 해야겠어요 








안녕하세요 기존에 Windows 7  에서 


selenium-js를 통해서 네이버 자동 로그인을 구현해서 예제로 놓았었는데요


// 네이버 자동로그인 windows 7 Chrome 66 Selenium-JS 4.0.0alpha

http://ipex.tistory.com/entry/webUI-Naver-Login-seleniumJS-iframe-%EB%8C%80%EC%B2%98-%EB%B0%A9%EC%95%88?category=770641


// 네이버 자동로그인 windows 7 IE 11 Selenium-JS 4.0.0alpha

http://ipex.tistory.com/entry/webUI-Naver-Login-seleniumJS-iframe-%EB%8C%80%EC%B2%98-%EB%B0%A9%EC%95%88-IE%EB%B2%84%EC%A0%84?category=770641


// 네이버 변경 되는 로그인 페이지 URL

https://m.blog.naver.com/PostView.nhn?blogId=naver_diary&logNo=221320011665&proxyReferer=



네이버 로그인 페이지가 보안성 강화를 통해서 변경 된다고 합니다.


자동화의 단점이 이렇게 나타나네요 유지보수를 해야할 시점이 이렇게 빨리 찾아 오다니요


네이버 로그인 페이지가 바뀌는 것에 맞춰서 8월에 다시 예제를 작성해서 올리도록 하겠습니다.


감사합니다.


+ Recent posts