1c/Ox
1
0
mirror of https://github.com/LazarenkoA/Ox.git synced 2025-11-23 21:33:13 +02:00

доработан пример скрипта. Отладка

This commit is contained in:
Артем
2025-11-06 22:07:39 +03:00
parent f41eaef9be
commit 3e8048c323
10 changed files with 252 additions and 60 deletions

View File

@@ -0,0 +1,107 @@
import { test, expect } from '@playwright/test';
test('Тест свод отчетов', async ({ page }) => {
test.setTimeout(100_000); // секунд только для этого теста
await page.goto('https://digital.zo.gov.ru/sko/ru/');
await page.locator('#userName').click();
await page.locator('#userName').fill('АдминистраторП');
await page.locator('.authPassInput').first().click();
await page.locator('#userPassword').click();
await page.locator('#userPassword').fill('Ho0de3vi');
await page.getByRole('button', { name: 'Войти' }).click();
await page.getByText('Ф. 0503737').nth(2).dblclick();
await close(page);
await page.waitForTimeout(100);
await page.getByText('Нормативно-справочная').click();
await page.waitForTimeout(100);
await page.getByText('Бюджеты').click();
await page.waitForTimeout(100);
await doubleClickRandomRow(page);
await closeButton(page, 'ФормаЗаписатьИЗакрыть')
const count = await page.locator('id$="_CommandButtonOK"');
if(count > 0) {
await page.locator('id$="_CommandButtonOK"').click();
await closeButton(page, 'ФормаЗаписатьИЗакрыть')
}
await page.waitForTimeout(500);
await page.getByText('Анализ данных').click();
await page.waitForTimeout(500); // пауза
await page.locator('#cmd_0_0_txt').click();
await page.waitForTimeout(200);
await close(page);
await page.getByText('Комплект отчетности').click();
await page.waitForTimeout(200);
await page.locator('#cmd_2_0_txt').click();
await page.waitForTimeout(1000);
await page.locator('a[id^="form"][id$="СформироватьОтчет"]').last().click();
await page.waitForTimeout(1000);
await close(page);
});
function randomString() {
return Math.random().toString(36).substring(2, 10);
}
async function closeButton(page, name) {
const elem = await page.locator(`a[id$="_${name}"]`);
await elem.waitFor();
const elems = await elem.elementHandles();
console.log(name, elems.length)
for (const btn of elems) {
const id = await btn.evaluate(el => el.id);
if (new RegExp(`^form\\d+_${name}$`).test(id)) {
await btn.click();
break; // кликнули первый подходящий
}
}
}
async function doubleClickRandomRow(page){
// Получаем все строки
const rows = await page.locator('div.gridBody div.gridLine').all();
if (rows.length === 0) {
console.log(`В списке нет строк`);
return
}
await page.waitForTimeout(200);
// Фильтруем только видимые строки
const visibleRows = [];
for (const row of rows) {
if (await row.isVisible()) {
// const text = await row.innerText();
// console.log(text);
visibleRows.push(row);
}
}
// Выбираем случайную из видимых
const randomIndex = Math.floor(Math.random() * visibleRows.length);
const targetRow = visibleRows[randomIndex];
// Двойной клик
await targetRow.dblclick({ timeout: 5000 });
console.log(`Выполнен двойной клик на строку с индексом: ${randomIndex} из ${rows.length}`);
}
async function close(page) {
try {
await page.locator('[id^="VW_page"][id$="headerTopLine_cmd_CloseButton"]').last().click({ timeout: 10000 });
} catch (error) {
console.warn('⚠️ Кнопка закрытия не найдена или не кликабельна:', error.message);
}
}

View File

@@ -22,7 +22,7 @@ type Worker struct {
ParallelTests int `json:"parallel_tests"`
ws WS `json:"-"`
client gen.WorkerClient `json:"-"`
mx sync.Mutex `json:"-"`
mx sync.RWMutex `json:"-"`
}
func (w *Worker) ChangeState(newState state) {
@@ -30,7 +30,10 @@ func (w *Worker) ChangeState(newState state) {
defer w.mx.Unlock()
w.Status = newState
w.sendWorkerToFront()
}
func (w *Worker) sendWorkerToFront() {
data, _ := json.Marshal(w)
if err := w.ws.WriteWSMessage(string(data)); err != nil {
log.Println("WriteWSMessage error:", err)
@@ -64,8 +67,11 @@ func (w *Worker) Stop() error {
}
func (w *Worker) SetTestScript(script string) error {
w.mx.Lock()
defer w.mx.Unlock()
w.Script = script
_, err := w.client.SetTestScript(context.Background(), &gen.SetTestScriptReq{Script: script})
_, err := w.client.SetTestScript(context.Background(), &gen.TestScript{Script: script})
return err
}
@@ -90,15 +96,32 @@ func (w *Worker) grpcStart(ctx context.Context, chanStatus chan<- WorkerStatus)
defer conn.Close()
w.client = gen.NewWorkerClient(conn)
w.grpcKeepalive(ctx, w.client, chanStatus)
w.grpcKeepalive(ctx, chanStatus)
}
func (w *Worker) grpcKeepalive(ctx context.Context, client gen.WorkerClient, chanStatus chan<- WorkerStatus) {
func (w *Worker) syncScript(ctx context.Context) {
w.mx.Lock()
defer w.mx.Unlock()
if w.Script == "" {
if script, err := w.client.GetTestScript(ctx, &gen.Empty{}); err == nil {
w.Script = script.Script
}
} else {
_, _ = w.client.SetTestScript(ctx, &gen.TestScript{Script: w.Script})
}
}
func (w *Worker) grpcKeepalive(ctx context.Context, chanStatus chan<- WorkerStatus) {
for {
stream, err := client.ObserverChangeState(ctx, &gen.Empty{})
stream, err := w.client.ObserverChangeState(ctx, &gen.Empty{})
if err != nil {
log.Println("GRPC error:", err)
} else {
// сразу при подключении синкаем скрипт потому что мог быть перезагружен воркер тогда мы скрипт отдает
// а мог быть перезагружен observer тогда мы скрипт забираем
w.syncScript(ctx)
chanStatus <- WorkerStatus{workerID: w.Id, status: gen.WorkerStatus_STATE_READY}
w.readStream(stream, w.Id, chanStatus)
}

View File

@@ -34,7 +34,7 @@ func NewGRPCServer(ctx context.Context, port int, worker gen.WorkerServer) error
func logInterceptor(logger *slog.Logger) func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) {
return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) {
_, err = handler(ctx, req)
resp, err = handler(ctx, req)
if err != nil {
logger.ErrorContext(ctx, "grpc error", "error", err)
} else {

View File

@@ -2,20 +2,22 @@ package app
import (
"context"
"errors"
"fmt"
"github.com/hashicorp/go-multierror"
"github.com/samber/lo"
"github.com/sourcegraph/conc"
)
func (w *Worker) startJob(ctx context.Context, testCount int32) error {
w.logger.InfoContext(ctx, fmt.Sprintf("start worker, test count %d", testCount))
err := new(multierror.Error)
var err error
var wg conc.WaitGroup
for range testCount {
wg.Go(func() {
if e := w.runTest(ctx, w.playwrightDir); e != nil {
err = multierror.Append(err, e)
w.logger.ErrorContext(ctx, e.Error())
err = lo.If(err == nil, errors.New("one or more tests failed with an error")).Else(err)
return
}
w.logger.InfoContext(ctx, "test is pass")
@@ -23,5 +25,5 @@ func (w *Worker) startJob(ctx context.Context, testCount int32) error {
}
wg.Wait()
return err.ErrorOrNil()
return err
}

View File

@@ -5,18 +5,23 @@ import (
"embed"
"fmt"
"github.com/pkg/errors"
"math/rand/v2"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"time"
)
//go:embed resource/*
var staticFS embed.FS
func (w *Worker) runTest(ctx context.Context, playwrightDir string) error {
w.logger.InfoContext(ctx, "exec run playwright test")
// небольшая рандомная задержка
time.Sleep(time.Duration(rand.IntN(5)) * time.Second)
w.logger.DebugContext(ctx, "exec run playwright test")
if strings.TrimSpace(w.script) == "" {
return errors.New("script not filled ")
@@ -87,15 +92,6 @@ func (w *Worker) create(ctx context.Context, rootDir string) error {
return nil
}
func replacePlaywrightConfig(rootDir string) error {
data, err := staticFS.ReadFile("resource/playwright.config.js")
if err != nil {
return err
}
targetPath := filepath.Join(rootDir, "playwright.config.js")
return os.WriteFile(targetPath, data, 0o644)
}
func (w *Worker) cmdRun(ctx context.Context, cmd *exec.Cmd) ([]byte, error) {
w.stopProcess(ctx, cmd)
@@ -128,6 +124,16 @@ func (w *Worker) cmdRun(ctx context.Context, cmd *exec.Cmd) ([]byte, error) {
return out, nil
}
func replacePlaywrightConfig(rootDir string) error {
data, err := staticFS.ReadFile("resource/playwright.config.js")
if err != nil {
return err
}
targetPath := filepath.Join(rootDir, "playwright.config.js")
return os.WriteFile(targetPath, data, 0o644)
}
// npx create-playwright@latest --quiet --lang=js --install-deps --gha
// npx playwright install
// npx playwright uninstall --all

View File

@@ -15,7 +15,7 @@ func (w *Worker) stopProcess(ctx context.Context, cmd *exec.Cmd) {
go func() {
<-ctx.Done()
w.logger.WarnContext(ctx, "context canceled -> terminating process group")
//w.logger.WarnContext(ctx, "context canceled -> terminating process group")
// Отправляем Ctrl-Break всей группе процессов
_ = windows.GenerateConsoleCtrlEvent(syscall.CTRL_BREAK_EVENT, uint32(cmd.Process.Pid))

View File

@@ -10,6 +10,7 @@ import (
"log/slog"
"os"
"path/filepath"
"sync"
)
type Worker struct {
@@ -20,6 +21,7 @@ type Worker struct {
playwrightDir string
script string
logger *slog.Logger
mx sync.RWMutex
}
func NewWorker() *Worker {
@@ -65,12 +67,21 @@ func dirExists(path string) bool {
return err == nil
}
func (w *Worker) SetTestScript(_ context.Context, req *gen.SetTestScriptReq) (*gen.Empty, error) {
w.script = req.Script
func (w *Worker) SetTestScript(_ context.Context, req *gen.TestScript) (*gen.Empty, error) {
w.mx.Lock()
defer w.mx.Unlock()
w.script = req.Script
return new(gen.Empty), nil
}
func (w *Worker) GetTestScript(_ context.Context, _ *gen.Empty) (*gen.TestScript, error) {
w.mx.RLock()
defer w.mx.RUnlock()
return &gen.TestScript{Script: w.script}, nil
}
func (w *Worker) Start(ctxParent context.Context, resp *gen.StartResp) (_ *gen.Empty, err error) {
defer func() {
if err != nil {

View File

@@ -161,27 +161,27 @@ func (x *StartResp) GetTestCount() int32 {
return 0
}
type SetTestScriptReq struct {
type TestScript struct {
state protoimpl.MessageState `protogen:"open.v1"`
Script string `protobuf:"bytes,1,opt,name=script,proto3" json:"script,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *SetTestScriptReq) Reset() {
*x = SetTestScriptReq{}
func (x *TestScript) Reset() {
*x = TestScript{}
mi := &file_worker_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *SetTestScriptReq) String() string {
func (x *TestScript) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SetTestScriptReq) ProtoMessage() {}
func (*TestScript) ProtoMessage() {}
func (x *SetTestScriptReq) ProtoReflect() protoreflect.Message {
func (x *TestScript) ProtoReflect() protoreflect.Message {
mi := &file_worker_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -193,12 +193,12 @@ func (x *SetTestScriptReq) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
// Deprecated: Use SetTestScriptReq.ProtoReflect.Descriptor instead.
func (*SetTestScriptReq) Descriptor() ([]byte, []int) {
// Deprecated: Use TestScript.ProtoReflect.Descriptor instead.
func (*TestScript) Descriptor() ([]byte, []int) {
return file_worker_proto_rawDescGZIP(), []int{2}
}
func (x *SetTestScriptReq) GetScript() string {
func (x *TestScript) GetScript() string {
if x != nil {
return x.Script
}
@@ -251,17 +251,19 @@ const file_worker_proto_rawDesc = "" +
"\x06status\x18\x04 \x01(\x0e2\r.WorkerStatusR\x06status\"*\n" +
"\tStartResp\x12\x1d\n" +
"\n" +
"test_count\x18\x04 \x01(\x05R\ttestCount\"*\n" +
"\x10SetTestScriptReq\x12\x16\n" +
"test_count\x18\x04 \x01(\x05R\ttestCount\"$\n" +
"\n" +
"TestScript\x12\x16\n" +
"\x06script\x18\x01 \x01(\tR\x06script\"\a\n" +
"\x05Empty*Z\n" +
"\fWorkerStatus\x12\x15\n" +
"\x11STATE_UNSPECIFIED\x10\x00\x12\x0f\n" +
"\vSTATE_READY\x10\x01\x12\x11\n" +
"\rSTATE_RUNNING\x10\x02\x12\x0f\n" +
"\vSTATE_ERROR\x10\x032\x9f\x01\n" +
"\x06Worker\x12,\n" +
"\rSetTestScript\x12\x11.SetTestScriptReq\x1a\x06.Empty\"\x00\x12\x1d\n" +
"\vSTATE_ERROR\x10\x032\xc1\x01\n" +
"\x06Worker\x12&\n" +
"\rSetTestScript\x12\v.TestScript\x1a\x06.Empty\"\x00\x12&\n" +
"\rGetTestScript\x12\x06.Empty\x1a\v.TestScript\"\x00\x12\x1d\n" +
"\x05Start\x12\n" +
".StartResp\x1a\x06.Empty\"\x00\x12\x18\n" +
"\x04Stop\x12\x06.Empty\x1a\x06.Empty\"\x00\x12.\n" +
@@ -285,21 +287,23 @@ var file_worker_proto_goTypes = []any{
(WorkerStatus)(0), // 0: WorkerStatus
(*StatusInfo)(nil), // 1: StatusInfo
(*StartResp)(nil), // 2: StartResp
(*SetTestScriptReq)(nil), // 3: SetTestScriptReq
(*TestScript)(nil), // 3: TestScript
(*Empty)(nil), // 4: Empty
}
var file_worker_proto_depIdxs = []int32{
0, // 0: StatusInfo.status:type_name -> WorkerStatus
3, // 1: Worker.SetTestScript:input_type -> SetTestScriptReq
2, // 2: Worker.Start:input_type -> StartResp
4, // 3: Worker.Stop:input_type -> Empty
4, // 4: Worker.ObserverChangeState:input_type -> Empty
4, // 5: Worker.SetTestScript:output_type -> Empty
4, // 6: Worker.Start:output_type -> Empty
4, // 7: Worker.Stop:output_type -> Empty
1, // 8: Worker.ObserverChangeState:output_type -> StatusInfo
5, // [5:9] is the sub-list for method output_type
1, // [1:5] is the sub-list for method input_type
3, // 1: Worker.SetTestScript:input_type -> TestScript
4, // 2: Worker.GetTestScript:input_type -> Empty
2, // 3: Worker.Start:input_type -> StartResp
4, // 4: Worker.Stop:input_type -> Empty
4, // 5: Worker.ObserverChangeState:input_type -> Empty
4, // 6: Worker.SetTestScript:output_type -> Empty
3, // 7: Worker.GetTestScript:output_type -> TestScript
4, // 8: Worker.Start:output_type -> Empty
4, // 9: Worker.Stop:output_type -> Empty
1, // 10: Worker.ObserverChangeState:output_type -> StatusInfo
6, // [6:11] is the sub-list for method output_type
1, // [1:6] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name

View File

@@ -20,6 +20,7 @@ const _ = grpc.SupportPackageIsVersion9
const (
Worker_SetTestScript_FullMethodName = "/Worker/SetTestScript"
Worker_GetTestScript_FullMethodName = "/Worker/GetTestScript"
Worker_Start_FullMethodName = "/Worker/Start"
Worker_Stop_FullMethodName = "/Worker/Stop"
Worker_ObserverChangeState_FullMethodName = "/Worker/ObserverChangeState"
@@ -29,7 +30,8 @@ const (
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type WorkerClient interface {
SetTestScript(ctx context.Context, in *SetTestScriptReq, opts ...grpc.CallOption) (*Empty, error)
SetTestScript(ctx context.Context, in *TestScript, opts ...grpc.CallOption) (*Empty, error)
GetTestScript(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*TestScript, error)
Start(ctx context.Context, in *StartResp, opts ...grpc.CallOption) (*Empty, error)
Stop(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*Empty, error)
ObserverChangeState(ctx context.Context, in *Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[StatusInfo], error)
@@ -43,7 +45,7 @@ func NewWorkerClient(cc grpc.ClientConnInterface) WorkerClient {
return &workerClient{cc}
}
func (c *workerClient) SetTestScript(ctx context.Context, in *SetTestScriptReq, opts ...grpc.CallOption) (*Empty, error) {
func (c *workerClient) SetTestScript(ctx context.Context, in *TestScript, opts ...grpc.CallOption) (*Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(Empty)
err := c.cc.Invoke(ctx, Worker_SetTestScript_FullMethodName, in, out, cOpts...)
@@ -53,6 +55,16 @@ func (c *workerClient) SetTestScript(ctx context.Context, in *SetTestScriptReq,
return out, nil
}
func (c *workerClient) GetTestScript(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*TestScript, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(TestScript)
err := c.cc.Invoke(ctx, Worker_GetTestScript_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *workerClient) Start(ctx context.Context, in *StartResp, opts ...grpc.CallOption) (*Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(Empty)
@@ -96,7 +108,8 @@ type Worker_ObserverChangeStateClient = grpc.ServerStreamingClient[StatusInfo]
// All implementations must embed UnimplementedWorkerServer
// for forward compatibility.
type WorkerServer interface {
SetTestScript(context.Context, *SetTestScriptReq) (*Empty, error)
SetTestScript(context.Context, *TestScript) (*Empty, error)
GetTestScript(context.Context, *Empty) (*TestScript, error)
Start(context.Context, *StartResp) (*Empty, error)
Stop(context.Context, *Empty) (*Empty, error)
ObserverChangeState(*Empty, grpc.ServerStreamingServer[StatusInfo]) error
@@ -110,9 +123,12 @@ type WorkerServer interface {
// pointer dereference when methods are called.
type UnimplementedWorkerServer struct{}
func (UnimplementedWorkerServer) SetTestScript(context.Context, *SetTestScriptReq) (*Empty, error) {
func (UnimplementedWorkerServer) SetTestScript(context.Context, *TestScript) (*Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method SetTestScript not implemented")
}
func (UnimplementedWorkerServer) GetTestScript(context.Context, *Empty) (*TestScript, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetTestScript not implemented")
}
func (UnimplementedWorkerServer) Start(context.Context, *StartResp) (*Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method Start not implemented")
}
@@ -144,7 +160,7 @@ func RegisterWorkerServer(s grpc.ServiceRegistrar, srv WorkerServer) {
}
func _Worker_SetTestScript_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SetTestScriptReq)
in := new(TestScript)
if err := dec(in); err != nil {
return nil, err
}
@@ -156,7 +172,25 @@ func _Worker_SetTestScript_Handler(srv interface{}, ctx context.Context, dec fun
FullMethod: Worker_SetTestScript_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(WorkerServer).SetTestScript(ctx, req.(*SetTestScriptReq))
return srv.(WorkerServer).SetTestScript(ctx, req.(*TestScript))
}
return interceptor(ctx, in, info, handler)
}
func _Worker_GetTestScript_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(WorkerServer).GetTestScript(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Worker_GetTestScript_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(WorkerServer).GetTestScript(ctx, req.(*Empty))
}
return interceptor(ctx, in, info, handler)
}
@@ -219,6 +253,10 @@ var Worker_ServiceDesc = grpc.ServiceDesc{
MethodName: "SetTestScript",
Handler: _Worker_SetTestScript_Handler,
},
{
MethodName: "GetTestScript",
Handler: _Worker_GetTestScript_Handler,
},
{
MethodName: "Start",
Handler: _Worker_Start_Handler,

View File

@@ -3,7 +3,8 @@ syntax = "proto3";
option go_package = "./gen";
service Worker {
rpc SetTestScript(SetTestScriptReq) returns(Empty) {}
rpc SetTestScript(TestScript) returns(Empty) {}
rpc GetTestScript(Empty) returns(TestScript) {}
rpc Start(StartResp) returns(Empty) {}
rpc Stop(Empty) returns(Empty) {}
rpc ObserverChangeState(Empty) returns(stream StatusInfo) {}
@@ -17,7 +18,7 @@ message StartResp {
int32 test_count = 4;
}
message SetTestScriptReq {
message TestScript {
string script = 1;
}